I have a custom component and one of the properties is a form. That property comes through as a string rather than a class.
In this thread: Form property of a Custom Component the issue is talked about and Meredydd mentions a couple of potential fixes that would need changes on Anvil’s end.
One was delivering the class instead of the string. That clearly isn’t happening yet.
Another was getting __import__
working in Skulpt. That also doesn’t seem to have happened.
Does anyone have a good mechanism for working with Form type properties? I’d ideally like to allow a form to be selected in the properties panel in the designer, and then convert that into an instance of the form in code.
Something similar happens in RepeatingPanels, where the template type is selected in the properties panel, but instances of it are created as items are added. But looking through the Anvil source hasn’t led to any revelations.
Here’s the obligatory clone link that shows the form property coming through as a string: Anvil | Login
I wonder if there’s a clue in this source code:
Possibly, but I haven’t used the new theme enough to know where to look, and some searching of the repo didn’t turn up anything concerning repeating panels.
I’ve been playing, and trying to work in the technique shown in the thread I linked above, and am getting issues with that. I use code like this to try to get the class out of the string:
if isinstance(self.template, str):
open_form(self.template)
self.template = get_open_form().__class__
But I get an error out of that:
ModuleNotFoundError: No module named 'Form2'
at TestComp, line 13
Apparently when running in the designer open_form
doesn’t work well, which makes sense given the context. But it eliminates the only workaround mentioned in the above thread.
One of Anvil’s goals with the new designer is to make something like a custom repeating panel possible. Basically anything that a native Anvil Component can do, could be written by an inclined Anvil developer.
The api for this is not yet public. So this post should be taken with a grain of salt.
You can use anvil.property_utils.get_form_constructor(component, form_property_value)
to get a form constructor from a form property
Here is a clone that implements a very simple custom component repeating panel
The code
from ._anvil_designer import CustomRepeaterTemplate
from anvil.property_utils import get_form_constructor
from anvil.designer import in_designer
class CustomRepeater(CustomRepeaterTemplate):
def __init__(self, **properties):
self._item_template = None
self._items = []
self.init_components(**properties)
def setup(self):
self.clear()
item_template = self.item_template
if not item_template:
return
constructor = get_form_constructor(self, item_template, prefer_live_design=False)
if in_designer:
items = (None for _ in range(3))
else:
items = self.items
for item in items:
self.add_component(constructor(item=item))
@property
def item_template(self):
return self._item_template
@item_template.setter
def item_template(self, value):
self._item_template = value
self.setup()
@property
def items(self):
return self._items
@items.setter
def items(self, value):
self._items = list(value) if value is not None else []
self.setup()
Note Skulpt’s __import__
is implemented and does work.
anvil extras uses the __import__
for its utils.import_module
2 Likes
Nice! That works beautifully. How subject to change is the get_form_constructor
API? I’m wondering how safe it’d be to use in a production app.
I also tried using __import__
, and got farther along than the last time. But it tells me the module can’t be found, so I’d assume there’s something I don’t understand about the scope it’s operating on. That’s just passing it the string from the form property, e.g. “Form1.ItemTemplate1”. I was trying to use it instead of the get_form_constructor
to see how it compares, and as an alternative if get_form_constructor
isn’t safe to use in production yet.
Yeah __import__
is not nice thing to work with. You probably need to use the Apps package name at the start of that e.g.
__import__("M3_App_10.Form1.ItemTemplate1")
Good question, I think you should go with the usual protocol of, if it’s not documented, it’s not stable.
That got me there. Here’s what works with everything hardcoded:
self._item_template_class = __import__("Efficient_Repeating_Panel.Form1", fromlist=["ItemTemplate"])
self._item_template_class = getattr(self._item_template_class, "ItemTemplate")
To do this in a non-hardcoded way, I parsed out the form property:
package_names = self.item_template.split(".")
form_name = package_names[-1]
package_names = ["Efficient_Repeating_Panel"] + package_names[:-1]
import_name = ".".join(package_names)
self._item_template_class = __import__(import_name, fromlist=[form_name])
self._item_template_class = getattr(self._item_template_class, form_name)
Definitely not as nice as get_form_constructor
, and it doesn’t look exactly the same in the designer. I’m assuming that’s the difference between instantiating the form directly vs with get_form_constructor
’s prefer_live_design=False
.
I think the last issue would be that when the code is in a dependency, the right app package name won’t be the dependency’s app. Is there a way for the dependency to get the package name of the app it’s running in via code? Or is that something that’s going to need to be a property of the custom component so it can be set in the designer?
1 Like
We do intend to support getting the app package name of the main app. It’s on our list for this sort of use case.
Prefer live design determines whether you instantiate the template like you would when running the app (live version) or use the version you would see if you opened that template in its own tab in the designer.
2 Likes
Okay, so trying to parameterize the app package name so it can be used as a dependency. That leads to this slight modification of the original code:
package_names = [self.app_package] + package_names[:-1]
basically just using a parameter on the custom component. This works fine when running the custom component’s app. But when running it as a dependency of another app, it generates an error because the result of this code:
self._item_template_class = __import__(import_name, fromlist=[form_name])
self._item_template_class = getattr(self._item_template_class, form_name)
is the form module instead of the form class.
For what it’s worth, get_form_constructor
works fine in a dependency. So if someone is reading this in the future and get_form_constructor
is an official part of the Anvil API, use it instead of mucking around with __import__
.
1 Like
Just popping in here to say - get_form_constructor
is very nearly stable! (I just need to check the code.) It’s explicitly there to enable third-party RepeatingPanel-alikes (and there’s even API surface to allow editing the form inline, like the real RepeatingPanel - it’s pretty cool, and we can’t wait to release it all.)
Overall, that function is unlikely to change (and if it changes, it will be functionally very similar), but it is not guranteed not to change.
3 Likes
Thanks Meredydd! I’m looking forward to the official announcement of those features.