guidata
guidata copied to clipboard
Is this a bug in ChoiceWidget ?
Hello, I am trying to make some code written by a scientific releasable.
I find this in the code
#these lines are needed to correct an error in guidata
import inspect
from guidata.dataset.qtitemwidgets import ChoiceWidget
string = inspect.getsource(ChoiceWidget.get)
ind0 = string.index('self._first_call = False')
ind1 = string.index('self.index_changed(idx)')
txt = "error not corrected in python module guidata.dataset.qtitemwidgets\n"
txt += "in ChoiceWidget.get(self):"
txt += "inverting lines 'self._first_call = False' and 'self.index_changed(idx)' is needed"
assert (ind0 < ind1),txt
do you have an opinion about this ???
Cheers
Fred
Hello Fred, Thanks for the feedback!
The fact it's needed to inverse those two lines as a workaround does not ring a bell, unfortunately. I'm not comfortable with changing this without a test case failure.
Could you provide a simple example that reproduce the bug reported by your colleague?
Maybe he/she is using a callback on a ChoiceItem, but I would really need to be able to reproduce it in order to fix it.
Now is a good time for a fix as I'm planning a maintenance release in the next days or so.
If there are callbacks for two ChoiceItem, and if each of these callbacks has an action on the other ChoiceItem, it creates a nested loop. The code, for get() is:
def get(self) -> None:
"""Update widget contents from data item value"""
self.initialize_widget()
value = self.item.get()
if value is not None:
idx = 0
_choices = self.item.get_prop_value("data", "choices")
for key, _val, _img in _choices:
if key == value:
break
idx += 1
self.set_widget_value(idx)
if self._first_call:
self.index_changed(idx)
self._first_call = False
In that case, each get() function is called alternatively by the two callbacks and self._first_call = False never occurs
Hi,
Thanks for the feedback. Could you please write a simple script demonstrating what is the current (wrong) behavior and helping understand what is the behavior you would expect?
Example of script that you could start with and add your own code to illustrate your bug report:
import guidata.dataset as gds
from guidata.env import execenv
from guidata.qthelpers import qt_app_context
class Parameters(gds.DataSet):
"""Example dataset"""
def cb_example1(self, item, value):
"""Example callback function #1"""
execenv.print("\n*** Callback #1 ***\nitem: ", item, "\nvalue:", value)
# Do something here
def cb_example2(self, item, value):
"""Example callback function #2"""
execenv.print("\n*** Callback #2 ***\nitem: ", item, "\nvalue:", value)
# Do something here
_choices = [
(16, "first choice"),
(32, "second choice"),
(64, "third choice"),
]
choice1 = gds.ChoiceItem(
"Choice item #1",
_choices,
default=64,
).set_prop("display", callback=cb_example1)
choice2 = (
gds.ChoiceItem(
"Choice item #2",
_choices,
default=32,
)
.set_pos(col=1, colspan=2)
.set_prop("display", callback=cb_example2)
)
def test_callbacks():
"""Test callbacks"""
with qt_app_context():
prm = Parameters()
prm.edit()
if __name__ == "__main__":
test_callbacks()
Hello,
When I wrote my code few years ago, I found the only way to work was
to invert the two lines in ChoiceWidget
In my code, I had a class derived from DataSet, SpotShapeParam, with
two callbacks: when changing the lattice, it changes the list of
choices for latticedomain
best regards
Geoffroy
class UpdateProp(ItemProperty): """An 'operator property' prop: ItemProperty instance func: function """ def init(self, prop = None, func = None): self.property = prop self.function = func
def __call__(self, instance, item, value): #DataSet instance,
DataItem instance, value of the dataitem self.function(instance, item, value) return self
def update_domain(spotshapeparam, choice_item, latticedomain):
"""update a SpotShapeParam instance parameters when lattice has
been changed"""
if spotshapeparam.lattice is not None:
if latticedomain is not None:
hx, kx =
spotshapeparam.lattice.get_node_coordinates(spotshapeparam.x0,
spotshapeparam.y0, latticedomain)
h = np.round(hx)
k = np.round(kx)
spotshapeparam._items[2].set_from_string(spotshapeparam, '%d'%h)
spotshapeparam._items[3].set_from_string(spotshapeparam, '%d'%k)
spotshapeparam._items[4].set_from_string(spotshapeparam, '%g'%hx)
spotshapeparam._items[5].set_from_string(spotshapeparam, '%g'%kx)
def update_lattice(spotshapeparam, choice_item, lattice):
"""update a SpotShapeParam instance parameters when lattice has
been changed"""
if lattice is not None:
#update the value of h and k with the corresponding lattice
spotshapeparam.latticedomain =
min(spotshapeparam.latticedomain,lattice.ndomains-1)
spotshapeparam.update_domain_choices(lattice)
#hx, kx = lattice.get_node_coordinates(spotshapeparam.x0,
spotshapeparam.y0, self.latticedomain)
if spotshapeparam.latticedomain is not None:
hx, kx = lattice.get_node_coordinates(spotshapeparam.x0,
spotshapeparam.y0, spotshapeparam.latticedomain)
h = np.round(hx)
k = np.round(kx)
spotshapeparam._items[2].set_from_string(spotshapeparam, '%d'%h)
spotshapeparam._items[3].set_from_string(spotshapeparam, '%d'%k)
spotshapeparam._items[4].set_from_string(spotshapeparam, '%g'%hx)
spotshapeparam._items[5].set_from_string(spotshapeparam, '%g'%kx)
class SpotShapeParam(DataSet):
"""DataSet for defining parameters of a SpotShape instance"""
x0 = FloatItem(("x0"), default = 1.)
y0 = FloatItem(("y0"), default = 0.)
h = IntItem(("h_integer"), default = 0)
k = IntItem(("k_integer"), default = 0)
hx = FloatItem(("h_float"), default = 0.)
kx = FloatItem(("k_float"), default = 0.)
lattice = ChoiceItem(("associated lattice"),(None,'None'))
#instance of LatticeGrid or ReconstructionGrid
lattice.set_prop("display", callback = UpdateProp(func = update_lattice))
latticedomain = ChoiceItem(("domain index"),(None,'None'))
latticedomain.set_prop("display", callback = UpdateProp(func =
update_domain))
def update_param(self, obj):
self.x0 = obj.x0
self.y0 = obj.y0
self.h = obj.h
self.k = obj.k
self.hx = obj.hx
self.kx = obj.kx
self.lattice = obj.lattice
self.latticedomain = obj.latticedomain
def update_shape(self, obj):
obj.set_pos(self.x0, self.y0)
obj.h = self.h
obj.k = self.k
obj.hx = self.hx
obj.kx = self.kx
obj.lattice = self.lattice
obj.latticedomain = self.latticedomain
self.update_param(obj)
def update_choices(self,lattices):
#lattices is the list of lattices and reconstructions in the plot
_choices = [self._items[6]._normalize_choice(None,'None')]
self._items[6] is a 'guidata.dataset.dataitems.ChoiceItem
for lattice in lattices:
label = lattice.shapeparam.label
_choices.append(self._items[6]._normalize_choice(lattice, label))
self._items[6].set_prop("data", choices = _choices) #we
update the property of the associated widget
def update_domain_choices(self, lattice):
#when a LatticeGrid/ReconstructionGrid instance has been
chosen, update the choices of domains if lattice is None: _choices = [(None,'None')] else: _choices = [] for idom in range(lattice.ndomains):
_choices.append(self._items[7]._normalize_choice(idom,'%d'%idom))
self._items[7].set_prop("data", choices = _choices)
Pierre Raybaut @.***> a écrit :
Hi,
Thanks for the feedback. Could you please write a simple script demonstrating what is the
current (wrong) behavior and helping understand what is the behavior
you would expect?Example of script that you could start with and add your own code to
illustrate your bug report:import guidata.dataset as gds from guidata.env import execenv from guidata.qthelpers import qt_app_context class Parameters(gds.DataSet): """Example dataset""" def cb_example1(self, item, value): """Example callback function #1""" execenv.print("\n*** Callback #1 ***\nitem: ", item, "\nvalue:", value) # Do something here def cb_example2(self, item, value): """Example callback function #2""" execenv.print("\n*** Callback #2 ***\nitem: ", item, "\nvalue:", value) # Do something here _choices = [ (16, "first choice"), (32, "second choice"), (64, "third choice"), ] choice1 = gds.ChoiceItem( "Choice item #1", _choices, default=64, ).set_prop("display", callback=cb_example1) choice2 = ( gds.ChoiceItem( "Choice item #2", _choices, default=32, ) .set_pos(col=1, colspan=2) .set_prop("display", callback=cb_example2) ) def test_callbacks(): """Test callbacks""" with qt_app_context(): prm = Parameters() prm.edit() if __name__ == "__main__": test_callbacks()-- Reply to this email directly or view it on GitHub: https://github.com/PlotPyStack/guidata/issues/75#issuecomment-2386210081 You are receiving this because you commented.
Message ID: @.***>
-- Geoffroy Prévot Directeur Adjoint Institut des NanoSciences de Paris 4, place Jussieu 75252 PARIS cedex 05 https://fr.linkedin.com/in/geoffroy-pr%C3%A9vot-493016233
Hi,
Thanks for the update. Unfortunately, given the use case, reading the code won't be enough to help investigate the issue. Would you please help me to write a standalone test script highlighting your specific use case?
So far, I've wrote the following but this is not working yet, but it should be a good start for you to try and reproduce the behavior you mentioned earlier:
# -*- coding: utf-8 -*-
#
# Licensed under the terms of the BSD 3-Clause
# (see guidata/LICENSE for details)
"""
Demonstrates how items may trigger callbacks when activated, or how
the list of choices may be dynamically changed.
"""
# guitest: show
import numpy as np
import guidata.dataset as gds
from guidata.env import execenv
from guidata.qthelpers import qt_app_context
def _(x):
return x
class ShapeParam(gds.DataSet):
"""DataSet for defining parameters of a shape"""
x0 = gds.FloatItem(_("x0"), default=1.0)
y0 = gds.FloatItem(_("y0"), default=0.0)
label = gds.StringItem(_("label"), default="default")
class LatticeGrid:
"""LatticeGrid class"""
def __init__(self, ndomains=1):
self.shapeparam = ShapeParam()
self.ndomains = ndomains
def get_node_coordinates(self, x0, y0, latticedomain):
return x0, y0
class UpdateProp(gds.ItemProperty):
"""An 'operator property'
prop: ItemProperty instance
func: function
"""
def __init__(self, prop=None, func=None):
self.property = prop
self.function = func
def __call__(self, instance, item, value):
self.function(instance, item, value)
return self
class SpotShapeParam(gds.DataSet):
"""DataSet for defining parameters of a SpotShape instance"""
def update_domain(self, choice_item, latticedomain):
"""update a SpotShapeParam instance parameters when lattice has been changed"""
if self.lattice is not None:
self.lattice: LatticeGrid
if latticedomain is not None:
hx, kx = self.lattice.get_node_coordinates(
self.x0, self.y0, latticedomain
)
h = np.round(hx)
k = np.round(kx)
self._items[2].set_from_string(self, "%d" % h)
self._items[3].set_from_string(self, "%d" % k)
self._items[4].set_from_string(self, "%g" % hx)
self._items[5].set_from_string(self, "%g" % kx)
def update_lattice(self, choice_item, lattice: LatticeGrid | None):
"""update a SpotShapeParam instance parameters when lattice has been changed"""
if lattice is not None:
# update the value of h and k with the corresponding lattice
self.latticedomain = min(self.latticedomain, lattice.ndomains - 1)
self.update_domain_choices(lattice)
if self.latticedomain is not None:
hx, kx = lattice.get_node_coordinates(
self.x0, self.y0, self.latticedomain
)
h = np.round(hx)
k = np.round(kx)
self._items[2].set_from_string(self, "%d" % h)
self._items[3].set_from_string(self, "%d" % k)
self._items[4].set_from_string(self, "%g" % hx)
self._items[5].set_from_string(self, "%g" % kx)
x0 = gds.FloatItem(_("x0"), default=1.0)
y0 = gds.FloatItem(_("y0"), default=0.0)
h = gds.IntItem(_("h_integer"), default=0)
k = gds.IntItem(_("k_integer"), default=0)
hx = gds.FloatItem(_("h_float"), default=0.0)
kx = gds.FloatItem(_("k_float"), default=0.0)
lattice = gds.ChoiceItem(
_("associated lattice"), ((None, "None"),)
) # instance of LatticeGrid or ReconstructionGrid
lattice.set_prop("display", callback=UpdateProp(func=update_lattice))
latticedomain = gds.ChoiceItem(_("domain index"), ((None, "None"),))
latticedomain.set_prop("display", callback=UpdateProp(func=update_domain))
def update_domain_choices(self, lattice):
# when a LatticeGrid/ReconstructionGrid instance has been chosen,
# update the choices of domains
if lattice is None:
_choices = [(None, "None")]
else:
_choices = []
for idom in range(lattice.ndomains):
_choices.append(self._items[7]._normalize_choice(idom, "%d" % idom))
self._items[7].set_prop("data", choices=_choices)
def test_callbacks():
"""Test callbacks"""
with qt_app_context():
prm = SpotShapeParam()
execenv.print(prm)
if prm.edit():
execenv.print(prm)
prm.view()
execenv.print("OK")
if __name__ == "__main__":
test_callbacks()
(There is an opportunity here because we are currently fixing bugs around guidata)
Hello
I remember that in a previous version of guidata, I had some trouble
with this __first_call property
that was never passing to False when having more than one ChoiceWidget
with an associated callback.
I had found the solution of inverting the lines.
With the new versions of guidata, this is no more the case, so I
cannot say anything more, this modification seems not to be necessary!
best regards
Geoffroy
Pierre Raybaut @.***> a écrit :
Hi,
Thanks for the update. Unfortunately, given the use case, reading the code won't be enough
to help investigate the issue. Would you please help me to write a standalone test script
highlighting your specific use case?So far, I've wrote the following but this is not working yet, but it
should be a good start for you to try and reproduce the behavior you
mentioned earlier:# -*- coding: utf-8 -*- # # Licensed under the terms of the BSD 3-Clause # (see guidata/LICENSE for details) """ Demonstrates how items may trigger callbacks when activated, or how the list of choices may be dynamically changed. """ # guitest: show import numpy as np import guidata.dataset as gds from guidata.env import execenv from guidata.qthelpers import qt_app_context def _(x): return x class ShapeParam(gds.DataSet): """DataSet for defining parameters of a shape""" x0 = gds.FloatItem(_("x0"), default=1.0) y0 = gds.FloatItem(_("y0"), default=0.0) label = gds.StringItem(_("label"), default="default") class LatticeGrid: """LatticeGrid class""" def __init__(self, shapeparam: ShapeParam, ndomains=1): self.shapeparam = shapeparam self.ndomains = ndomains def get_node_coordinates(self, x0, y0, latticedomain): return x0, y0 class UpdateProp(gds.ItemProperty): """An 'operator property' prop: ItemProperty instance func: function """ def __init__(self, prop=None, func=None): self.property = prop self.function = func def __call__(self, instance, item, value): self.function(instance, item, value) return self class SpotShapeParam(gds.DataSet): """DataSet for defining parameters of a SpotShape instance""" def update_domain(self, choice_item, latticedomain): """update a SpotShapeParam instance parameters when lattice has been changed""" if self.lattice is not None: self.lattice: LatticeGrid if latticedomain is not None: hx, kx = self.lattice.get_node_coordinates( self.x0, self.y0, latticedomain ) h = np.round(hx) k = np.round(kx) self._items[2].set_from_string(self, "%d" % h) self._items[3].set_from_string(self, "%d" % k) self._items[4].set_from_string(self, "%g" % hx) self._items[5].set_from_string(self, "%g" % kx) def update_lattice(self, choice_item, lattice: LatticeGrid | None): """update a SpotShapeParam instance parameters when lattice has been changed""" if lattice is not None: # update the value of h and k with the corresponding lattice self.latticedomain = min(self.latticedomain, lattice.ndomains - 1) self.update_domain_choices(lattice) if self.latticedomain is not None: hx, kx = lattice.get_node_coordinates( self.x0, self.y0, self.latticedomain ) h = np.round(hx) k = np.round(kx) self._items[2].set_from_string(self, "%d" % h) self._items[3].set_from_string(self, "%d" % k) self._items[4].set_from_string(self, "%g" % hx) self._items[5].set_from_string(self, "%g" % kx) x0 = gds.FloatItem(_("x0"), default=1.0) y0 = gds.FloatItem(_("y0"), default=0.0) h = gds.IntItem(_("h_integer"), default=0) k = gds.IntItem(_("k_integer"), default=0) hx = gds.FloatItem(_("h_float"), default=0.0) kx = gds.FloatItem(_("k_float"), default=0.0) lattice = gds.ChoiceItem( _("associated lattice"), (None, "None") ) # instance of LatticeGrid or ReconstructionGrid lattice.set_prop("display", callback=UpdateProp(func=update_lattice)) latticedomain = gds.ChoiceItem(_("domain index"), (None, "None")) latticedomain.set_prop("display", callback=UpdateProp(func=update_domain)) def update_domain_choices(self, lattice): # when a LatticeGrid/ReconstructionGrid instance has been chosen, # update the choices of domains if lattice is None: _choices = [(None, "None")] else: _choices = [] for idom in range(lattice.ndomains): _choices.append(self._items[7]._normalize_choice(idom, "%d" % idom)) self._items[7].set_prop("data", choices=_choices) def test_callbacks(): """Test callbacks""" with qt_app_context(): prm = SpotShapeParam() execenv.print(prm) if prm.edit(): execenv.print(prm) prm.view() execenv.print("OK") if __name__ == "__main__": test_callbacks()-- Reply to this email directly or view it on GitHub: https://github.com/PlotPyStack/guidata/issues/75#issuecomment-2475819267 You are receiving this because you commented.
Message ID: @.***>
-- Geoffroy Prévot Directeur Adjoint Institut des NanoSciences de Paris 4, place Jussieu 75252 PARIS cedex 05 https://fr.linkedin.com/in/geoffroy-pr%C3%A9vot-493016233
Thanks for the good news. I'm closing the issue.