Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIFields and dataclasses: near-term roadmap for magicgui #474

Open
3 of 7 tasks
tlambert03 opened this issue Oct 23, 2022 · 6 comments
Open
3 of 7 tasks

UIFields and dataclasses: near-term roadmap for magicgui #474

tlambert03 opened this issue Oct 23, 2022 · 6 comments

Comments

@tlambert03
Copy link
Member

tlambert03 commented Oct 23, 2022

Just wanted to lay out in writing some of the near-term plans that I have for magicgui:

Widget "fields" not "parameters"

I regret that function signatures were chosen as the most common way to instantiate and present a group of widgets in magicgui. I think it is one or two levels "too high". A think a better parallel would have been dataclasses. So many things fall into the general pattern of name: annotation = default value beyond just functions, such as typing.NamedTuple, typing.TypedDict, dataclasses.dataclass, pydantic.BaseModel, attrs.define... and, yes, function signatures.

Rather than framing a widget as having a relation to an inspect.Parameter (see magicgui/signature.py), i think a better analogue would have been dataclasses.Field. Currently, if one wants to make a compound widget in magicgui, you either manually construct it using the direct widget API with Container and create_widget, or you create a function and use @magicgui (which then uses inspect.signature and widgets are created for each Parameter with MagicParameter.to_widget()).

I've seen people do this:

@magicgui
def dummy_function(name: str, age: int = 0): ...

...just to create a widget. Which is a good indicator that the abstraction was too specific. There are plenty of use cases where one just wants to collect some data without necessarily passing them all to a specific function.

a collection of UiFields

In upcoming PRs, I'll be creating a "parallel API" (probably under a v2 or datagui namespace) that instead adds a magicgui.UiField object, (akin to dataclasses.Field, or pydantic.fields.ModelField or attrs.Attribute) that stores the metadata associated with a widget. This will still applicable to function signatures, but will make it much easier to construct a widget using a dataclass, or pydantic model, or attrs class, etc...:

# all of the representations we could easily support

@dataclass
class Person:
    name: str
    age: int = 0

@attrs.define
class Person:
    name: str
    age: int = 0

class Person(BaseModel):
    name: str
    age: int = 0

class Person(typing.NamedTuple):
    name: str
    age: int = 0

class Person(typing.TypedDict):
    name: str
    age: int

# functions are just a special case
def Person(name: str, age: int = 0):
    ...

# JSON Schema
Person = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "number", "default": 0},
    }
}

create_widget(Person)

under the hood, each of these is reduced to a gui model that consists of a sequence of UIFields:

model = [UiField(name='name', type=str), UiField(name='age', type=int, default=0)]

and then to make a widget:

container = Container(widgets=[field.create_widget() for field in model])

what about @magicgui for functions

Functions are still easy: you can always connect a callback to be called with the current values of the widgets... but it should be much easier to create a widget representing a set of fields without having to associate them with some callback. So none of this needs to affect the use case for functions... it's more about how we conceive of the underlying model here.

JSON schema connection

I'd like UiField to have a direct parallel with other schema languages, with JSON schema being the reference. So, all of the keywords in the validation vocabulary (things like maximum, maxLength, multipleOf, etc...) would have direct parallels in the UiField object (similar to a pydantic FieldInfo object). This would provide an easier connection with non-python representations of data schema, and would make it easier to do things in browsers (e.g. jupyter) with one of many javascript libraries that create UIs from schemas.

TODO:

@tlambert03
Copy link
Member Author

tlambert03 commented Oct 23, 2022

cc @hanjinliu, @Czaki, @brisvag, @dstansby, @jni, @gselzer .... lemme know if you have any broad thoughts here

@brisvag
Copy link
Contributor

brisvag commented Oct 24, 2022

Love this! It came up before several times, and I'm one of the many who use magicgui in the way you described. Couldn't be more on board!

@Czaki
Copy link
Contributor

Czaki commented Oct 24, 2022

It looks like something that will allow replacing part of the old PartSeg code. I like it.

@hanjinliu
Copy link
Contributor

I totally agree with these updates!

One thing I concern is that users still have to rely on insert for PushButton.
Of course one can do

class Person:
    button = SomeField(widget_type=PushButton)

but,

  • the 'button' attribute is completely redundant when class Person is used without GUI.
  • push buttons need callback.

I think the direct widget API still needs more emphasis.

@brisvag
Copy link
Contributor

brisvag commented Oct 25, 2022

class Person:
    button = SomeField(widget_type=PushButton)

Yeah, this feels odd... My intuition would want something like this:

class Person:
	@button
   def callback(self): ...

@tlambert03
Copy link
Member Author

Yeah, I recognize that buttons are still problematic. They are, of course, fundamentally a bit different than all of the other ValueWidgets in that they don't (really) contain a value. So, keeping with that, I don't see them as part of the dataclass & UiField abstraction per se... but rather something that would come on top of it / in addition to it.

I do like something along the lines of the @button decorator in @brisvag's example above. We can definitely do something like that:

@dataclass
class Person:
    name: str
    age: int = 0

    @button(label='Say Hi', order=0)
    def _say_hi(self):
        print(f"hi {self.name}!")

then:

model = build_model(Person)
# [
#    UiField(name='_say_hi', label='Say Hi', widget='PushButton', on_click=Person._say_hi)
#    UiField(name='name', type=str), 
#    UiField(name='age', type=int, default=0),
# ]
#  ... or something like that

I think the direct widget API still needs more emphasis.

I agree with this, and that would be part of the docs update mentioned in the original post

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants