Error binding second property on Custom Component to a label

Boy, here we go…

Context:
I have a custom component LabelTextBox composed of 3 main components. label_title, label_error, and text_box. Currently I have two custom properties title and error. Both are string types.

I was able to successfully bind the text of label_title to title and update the binding in a custom setter like this:

  @property
  def title(self):
    return self._title

  @title.setter
  def title(self, value):
    self._title = value
    self.refresh_data_bindings()

What I want to happen:
Be able to add arbitrary number of properties and custom setters, and refresh the data bindings on all the setters without losing all the other properties.

What I’ve tried and what’s not working:

Code Sample:

  @property
  def error(self):
    return self._error
  
  @error.setter
  def error(self, value):
    self._error = value
    self.refresh_data_bindings()

After adding the binding to label_error, and then adding the above code, I get this strange error message: AttributeError: 'LabelTextBox' object has no attribute 'error'

I don’t understand why I’m getting this error. Been banging my head against the wall all morning on this one. Any help is appreciated as always.

Clone link:
https://anvil.works/build#clone:2MS3OL6MMX5QG3ZC=OERB5BGFJOOQYBLVWTMZGJ2X

It seems to have to do with not initializing self._error in the component to anything. When I add this to init it works:

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    self.label_error.foreground = 'red'
    self._error=''
    self.init_components(**properties)

Why it works with title and not error I don’t know.

When a custom component assigns the properties it goes through each one assigning the value.
When you call refresh_data_bindings it calls each binding expression.

So the title is set.
This calls the refresh_data_bindings method
which also refreshes the error label binding
which tries to get the value for self._error
which hasn’t been set yet. :disappointed:

There may be something in our internal implementation that would have improved this so we’ll take a look.

A common pattern I’ve ended up using is the following:

default_props = {
  "title": "",
  "error": ""
}

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    properties = default_props | properties
    self._props = properties
    self.init_components(**properties)
    
  @property
  def title(self):
    return self._props['title']

  @title.setter
  def title(self, value):
    self._props['title'] = value
    self.refresh_data_bindings()

I do it this way with default_props since it makes creating LabelTextBox from code more straight forward.
It will also prevent the AttributeError since all the private attributes are set inside the _props object before the call to init_components


Sidenote:
If doing the property getters and setters is tedious at some point you might end up extracting them into common function e.g.

def component_binding_prop(prop_name):
  def fget(self)
    return self._props[prop_name]
  def fset(self, value):
    self._props[prop_name] = value
    self.refresh_data_bindings()
  return property(fget, fset)

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    properties = default_props | properties
    self._props = properties
    self.init_components(**properties)
  
  title = component_binding_prop('title')
  error = component_binding_prop('error')
2 Likes

Mmmh… this looks dangerous, either the default properties or the dictionary passed to the constructor risk to be changed:

Perhaps there is no risk because self._prop will never change, but this version feels safer:

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    properties = default_props | properties
    self._props = properties.copy()

default_props | properties

merges the two dictionaries into a new dictionary :wink:
So the call to copy would be redundant (PEP 584 – Add Union Operators To dict | peps.python.org)

props = default_props | properties
props is properties # False
props is default_props # False

it’s effectively the same as

props = {**default_props, **properties}

Oh, right, I thought it was an or ||, not an union |. I’m getting old :upside_down_face:

1 Like

So… for 1 property binding, I do it 1 way, and for N number of property bindings, I have to use a special pattern?

Even when I set the error property in the Startup form, I get the error :
AttributeError: ‘LabelTextBox’ object has no attribute ‘error’

Error binding [text of label_error](javascript:void(0)) to self.error:'LabelTextBox' object has no attribute 'error'

at [LabelTextBox, line 16](javascript:void(0)) called from [LabelTextBox, line 7](javascript:void(0))

So the title is set.
This calls the refresh_data_bindings method
which also refreshes the error label binding
which tries to get the value for self._error
which hasn’t been set yet. :disappointed:

This indicates to me that this is a bug, or a feature request. On the backend, Anvil should be automatically setting the private _x vars, so I don’t have to do all of this tedious rigmarole.

Thanks for the workaround in the meantime.

It might also be a documentation issue rather than a bug.

The general advice here is really to set private attributes before calling init_components.
Which is good practice in general with property getters and setters.

Think of the call to init_components as expecting all private attributes, which are used in data bindings, to exist.
With a single attribute that’s fine because you call the setter before the getter.
But with n attributes you call some getters before the setters
(when using the refresh_data_bindings() approach).

Both @jshaffstall suggestion and my suggestion stick to this rule of thumb
i.e. setting private attributes before calling init_components.

@jshaffstall suggestion does this specifically for self._error
(you’d probably want to do this for self._title too.

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    # set private attributes used in data bindings
    self._title = ""
    self._error = ""
    # do init_components
    self.init_components(**properties)

What i’ve suggested puts all the private attributes inside self._props before the call to self.init_components()

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    # set private attributes used in data bindings
    self._props = properties.copy()
    # do init_components
    self.init_components(**properties)

Both approaches are equivalent really.


Other approaches to consider depending on preference

Use a flag to avoid calling refresh_data_bindings until init_components() is done
class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    self._init = False
    self.init_components(**properties)
    # other code
    self._init = True
    
  @property
  def title(self):
    return self._title

  @title.setter
  def title(self, value):
    self._title = value
    if self._init:
      self.refresh_data_bindings()

  @property
  def error(self):
    return self._error

  @error.setter
  def error(self, value):
    self._error = value
    if self._init:
      self.refresh_data_bindings()


Don't use data bindings but access the properties directly in code
class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    
  @property
  def title(self):
    return self._title

  @title.setter
  def title(self, value):
    self._title = value
    self.label_title.text = value

  @property
  def error(self):
    return self._error

  @error.setter
  def error(self, value):
    self._error = value
    self.label_error.text = value

Don't use private attributes, just use component properties

(this approach is particularly useful for a TextBox/CheckBox when the property can be changed by the user)

class LabelTextBox(LabelTextBoxTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    
  @property
  def title(self):
    return self.label_title.text

  @title.setter
  def title(self, value):
    self.label_title.text = value

  @property
  def error(self):
    return self.label_error.text

  @error.setter
  def error(self, value):
    self.label_error.text = value

3 Likes