kalekundert / byoc

MIT License
0 stars 0 forks source link

Make apps more composable. #42

Open kalekundert opened 2 years ago

kalekundert commented 2 years ago

Specifically, I'd often want to create stepwise protocols that basically just call two or three other protocols. The question is how to configure those "contained" protocols. From python it's not an issue, since I can just access attributes like so: parent.child.param. But I'm not sure how to handle command-line options, config files, etc.

In this example, the child apps would be fully-functional apps in their own right, capable of parsing their own command lines and everything. These apps would also have no knowledge of the parent app. The parent may want to change argument names/config file keys/etc., and perhaps everything about how the value is loaded.

I guess one way to do this is just to have the parent make subclasses of any child it wants to use, and the subclasses can change how the various parameters are loaded, e.g.:

class Parent(byoc.App):

    class Child:
        x = param('X')

    def __bareinit__(self):
        self.child = self.Child.from_bare()
        byoc.replace_configs(self, self.child)  # This doesn't exist yet...

        # Might also be nice to make a helper for the above two lines:
        # self.child = byoc.make_child_app(self, self.Child)
        # self.child = self.Child.from_parent(self)

class Child(byoc.App):
    x = param('x')

It's actually kind of hard to imagine a better syntax than this. Users can still provide instances of the original child class and they will work flawlessly. By default all configs are loaded in the same way as in the child app, but anything can be changed. It might be nice to add an easy way to say "use a different command-line argument for this parameter, but do everything else the same". Maybe an Inherit getter, or something like inherited_param(keep=[], drop=[], append=[], prepend=[], insert={}, replace={}).

Now that I've written this all down, I think the existing syntax handles the "composing apps" use-case pretty well. I'll still submit this issue, as a reminder to add the various helpers discussed above, and to remind myself how to do this.

kalekundert commented 2 years ago

Related idea: Make it easier to a parent app to control which configs child apps use. Specifically, imagine a child app that uses DocoptConfig for processing command-line arguments. The parent might want to expose some of those arguments (perhaps with different names), but doesn't want to explicitly forbid all the others. What I have in mind is:

This requires giving the config classes some degree of control over how Key works. The most powerful way to do this would be to add a Config.is_match(key, log) method that does whatever it wants to decide how a key should be interpreted. I'm a little worried that this could be too powerful, though.

An alternative is to add a match_subclasses parameter to the base Config class. Then, you could make "strict" configs like so:

class Parent:

    class DocoptConfig(DocoptConfig):
        match_subclasses = False

    class Child(Child):
        pass

    __config__ = [DocoptConfig]