Custom Component with Object Custom Prop and setter doesn't have default value

What I’m trying to do:
I’ve got a custom component with a property that’s of type object. Normally, if I haven’t set that property, the default value is None (good). But, when I add a setter function, the property doesn’t get automatically set and I get errors trying to access it:

You can see the difference if you comment out the property decorated functions.

This seems like a bug.

Not a bug i’m afraid, but i agree it’s not obvious.

object properties are always listed as set at runtime.
If we started injecting None values during init_components, there’s potentially a lot of code out there that would break.
(existing setters would not be expecting None values, since they’ve never had to before)

The typical way i like to handle this


class CustomComponent(CustomComponentTemplate):
    def __init__(self, **properties):
        self._props = properties
        self.init_components(**properties)

    @proeprty
    def my_prop(self):
        return self._props.get("my_prop")

    @my_prop.setter
    def my_prop(self, value):
        self._props["my_prop"] = value
        ...

This is approximately how the underlying Custom component property setters and getters work.

I don’t understand. You already inject None in – just when I haven’t set my own property setter. I was working off this assumption when I moved my init code for object properties into a property setter function.

My point isn’t that None isn’t the default for any object property, but rather that it is only not the default when I make my own setter.

What do we mean by inject None in?

We don’t inject it in, not really.
The form template has it’s own descriptors with custom getters and setters.
The main difference is that they know ahead of time the default value.
So if a value is NOT set for a custom component property (in the case of an object property), the custom component property returns the default value.

>>> form = CustomComponent()
>>> TemplateClass = CustomComponent.__base__
>>> TemplateClass.my_prop
<CustomComponentProperty object>
>>> TemplateClass.my_prop.__get__(form, type(form))
None
# this is the default value
# my_prop was never actually set on the form instance
# and so my_prop getter returns the default value when asked

Here is some more pseudo code for the CustomComponentProperty descriptor class
(In reality these classes are defined in javascript, but this is largely how they look)

NOT_FOUND = object()

class CustomComponentProperty:
    def __init__(self, type, default_value=None):
        self._type = type
        self._default_value = default_value

    def __set_name__(self, owner, name):
        self._name = name

    def __get__(self, obj, obj_type=None):
        if obj is None:
            return self
        rv = obj._private_custom_props.get(self._name, NOT_FOUND)
        if rv is NOT_FOUND:
             return self._default_value
        return rv

    def __set__(self, obj, value):
        obj._private_custom_props[self._name] = value


class CustomComponentTemplate(ColumnPanel):
    my_prop = CustomComponentProperty("object")
    ...


When you override a custom component property you take responsibility for the getters and the setters.
And if you take responsibility, you probably want to make sure the getter will still work if the setter was never run.


Side Note, you could pass these onto super but it’s not particularly nice


class CustomComponent(CustomComponentTemplate):
    def __init__(self, **properties):
        self._super_base = super(CustomComponent, type(self))
        self.init_components(**properties)

    @property
    def my_prop(self):
        return self._super_base.my_prop.__get__(self, type(self))

    @my_prop.setter
    def my_prop(self, value):
        self._super_base.my_prop.__set__(self, value)
        ...

I guess you could also do some fun things with creating your own custom component property override descriptor class, but i’ll leave that as an exercise to the reader.

1 Like

By inject in, I mean that the default value if my code hasn’t passed a value to an obj custom prop is None unless I override the getter/setter. I would assume getters/setters shouldn’t affect default props (and they don’t for other custom prop types). So this is an outlier.

That makes more sense, but I don’t get a property not found error when I have custom getters and setters for other types of custom props (i.e. strings). Isn’t the point of setting the custom prop in the IDE/YAML so that it will always be there and I don’t have to worry about using getattr, etc. when writing my code – I can just trust it’s been initialized.

yes for all other property types except "object" the properties dict will include the values from the designer (or the default value if no value was set).

But for "object" types the properties dict will not include that item.
Mostly because "object" types are expected to be set at runtime.
(You’ll see this description in the designer next that property)

The place where we call the setters is in the init_components method.
And since the "object" property is not included in the properties dict, the "object" property is not set during the init_components method.

I agree that this might be considered incorrect behaviour. And perhaps a None value would have been better than no value.

But since this is the way it’s always been, it’s difficult to change this behaviour.
Any custom setter, that is overriding a property that is of type object, may never have handled a None value before, because "object" properties have always been “set at runtime”.


So unfortunately the right way to handle this is some defensive programming. Your custom property getters should work if the setter were never called.


We have internally talked about APIs that would help with the boiler plate here. But they’re difficult to get right. Something that allows you to do something when a property is set, without having to deal with the boiler plate of assigning the value to a private attribute.

class CustomComponent(CustomComponentTemplate):

    @post_setter("my_prop")
    def my_prop_post_setter(self):
         ...

You can actually create something that works quite well for the above api idea.
Happy to share these if they’re of interest.

here’s a related thread

So you are saying this is all expected:

  1. Object custom props get a default None when there’s no custom getter/setter
  2. Object custom props don’t exist when there’s a custom getter/setter and the code hasn’t set the custom prop (yet)

Yeah, I’d be willing to take a look at a potential contribution if you know where that would be at :slight_smile:

That FR actually doesn’t interest me, I like controlling when/how the custom props change my custom component, :slight_smile:

Yes it’s all expected.

Although i’d change the way we talk about this.

  1. Custom components get a descriptors for each custom component property whose __get__ returns None (or the default_value) if no value was set.

  2. When you write your own property getters and setters, you are overriding the underlying descriptor and so need to handle the get and set logic accordingly.

and then finally, expect object types to be set at runtime, so the setter won’t get called during init_components. But it’s probably a good idea to consider writing all your getters in a way that doesn’t require the setter to be called.

from ._anvil_designer import CustomCompTemplate


def set_obj_defaults(*obj_prop_names):
  def inner_default_setter(cls):
    orig_init = cls.__init__

    def __init__(self, *args, **kwargs):
      # set default attributes before init runs
      for prop in obj_prop_names:
        setattr(self, prop, None)
      orig_init(self, *args, **kwargs)

    cls.__init__ = __init__
    return cls

  return inner_default_setter


@set_obj_defaults("setter_test_obj")
class CustomComp(CustomCompTemplate):
  def __init__(self, **properties):
    self.init_components(**properties)
    # This prop doesn't have a setter so it won't error.
    print("here should be None without setter", self.test_obj)

    # This prop has a setter so it will error.
    print("here should be None even with setter", self.setter_test_obj)

  @property
  def setter_test_obj(self):
    return self._setter_test_obj

  @setter_test_obj.setter
  def setter_test_obj(self, value):
    self._setter_test_obj = value

I was able to whip this up with ChatGPT’s assistance. Is there anyway to get the object props from the YAML at runtime so I don’t have to manually keep the list up to date?

Not documented but you can use the _anvil_properties_ attribute on a component class. Which will return a list of property definitions. Each should have a "type" key.

1 Like
def set_obj_defaults(cls):
  orig_init = cls.__init__

  def __init__(self, *args, **kwargs):
    # set default attributes before init runs
    print(cls._anvil_properties_)
    for prop in cls._anvil_properties_:
      if prop["type"] == "object":
        setattr(self, prop["name"], None)
    orig_init(self, *args, **kwargs)

  cls.__init__ = __init__
  return cls

Any suggestions? Also, how stable is _anvil_properties_ ?

It’s fairly stable. You’ll find some uses of it in the m3 theme codebase and even in anvil_extras. It should get documented in the future.

No suggestions. Plenty of other fun ways to achieve this.

2 Likes