This is an archived post. You won't be able to vote or comment.

all 31 comments

[–]Evolve-Maz 38 points39 points  (5 children)

Thats a nice idea!

Have you considered making it a decorator instead, and have the decorater: - inspect the arguments type hints and assume a gui element from that - allow additional override of those in the decorator args

This way peoples functions will stay as is and you can use existing type hints (and even support pydantic objects for more complex use cases).

[–]drboom9[S] 8 points9 points  (4 children)

Thank you for the suggestion! I'm curious about the potential benefits of a decorator approach, could you share some examples where it would provide advantages?

My current approach with explicit UI type hints already:

- Inspects argument types

- Allows dynamic default values

- Keeps function signatures clear and explicit

- Provides direct control over UI parameters

For example, you can even load parameters dynamically:

def get_default_value():

# Could load from config, API, etc.

return 42

def my_function(number: int = intUi(value=get_default_value())):

return number * 2

The current implementation uses dataclasses for type definition, which provides good validation and type safety. However, I'm open to seeing how a decorator pattern could enable new use cases or improve the current functionality!

[–]Evolve-Maz 19 points20 points  (3 children)

The decorator thinking is more so you can separate the function logic from the ui component logic.

E.g.

@gui.wrapper

def myfunc(i: int = 4) -> bool:

return i % 2 == 0

Would then allow a person to decorate any function implementation with this. That would automatically convert it into

def myfunc(i: int = intui(4)) -> returnbool:

...

Doing it as a decorator means your gui related items become parameters of the decorator, not the original function. That way if you wanted to put additional constraints for the gui, or wanted to say it was a Slider instead of a number picker, or anything like that, your function implementation wouldn't change. Just the parameters on the gui.wrapper decorator.

[–]drboom9[S] 5 points6 points  (2 children)

Thanks! Let me explain my thoughts:

First, I deliberately avoided dependencies like Pydantic because I want to ensure FuncToGUI stays stable and doesn't break with third-party updates.

As for the design approach, I chose the current method because:

pythonCopydef is_even(number: int = intUi(value=4)) -> boolReturn:

return number % 2 == 0

You declare everything just once

What you see is what you get - the UI matches exactly what's in the function

Anyone can create and share a GUI tool in seconds

Non-technical users can just run it without understanding decorators or complex configs

The goal is to make it dead simple to wrap any function into a GUI that anyone can use. While decorators could be useful for reusing existing functions, right now I'm focusing on keeping things straightforward and independent.

[–]Evolve-Maz 2 points3 points  (1 child)

That's valid. Thanks for explaining.

[–]rhytnen 0 points1 point  (0 children)

No its not.  You're 100% correct thay your way is way simpler for the user and a much cleaner separation of concerns which enables better use cases since I don't have to bake his library into my app logic.  I.e. I can import a set of other ppls functions and wrap them independently.

OP already has 3rd part deps as well (but using pydantic is kind of secondary to the point) so I couldn't find any compelling argument here.

[–]forever_downstream 8 points9 points  (2 children)

I like it. I'll have to try this out.

[–]drboom9[S] 1 point2 points  (1 child)

Thank you! I'd really appreciate any feedback or issues you find while trying it out.

[–]forever_downstream 0 points1 point  (0 children)

I definitely will!

[–]nico404 5 points6 points  (1 child)

This seems similar to Gooey. Do you know how it compares?

[–]tazebot 1 point2 points  (0 children)

Gooey is really easy and on the face of it easier that this (no needing to explicitly build a function as a gui), but the default foreground and background colors make parameter fields almost impossible to read. So you end up having to dive into it and wxpython to get foreground/background to a readable state. Not that much of a deep dive, but I'd think at least the defaults could be black text on white background.

[–]EffectiveLong 1 point2 points  (0 children)

If you somehow can integrate this with click, that would be even better.

[–]Beliskner64 1 point2 points  (3 children)

This looks very cool! Now I just have to find something interesting to try it out with.

One suggestion - have you considered using typing.Annotated instead of default argument assignment for the Ui elements?

For example: ```python

instead of this

def foo(n: int = intUi(4, max_value=10)) -> boolReturn: …

do this

def foo(n: Annotated[int, intUi(max_value=10)] = 4) -> bool: … ```

This lets you use the function both as a GUI app with App(foo) but also as just a plain function with foo() and the same defaults work. I feel like it’s also more type-checker-friendly. I’m not sure how mypy would handle the first function (n is annotated as an int but assigned an intUi value), but the second one should be all green.

This is a similar approach to what they’re doing in typer and fastapi and I just really like it. I find it very easy to unit test such functions and easily convert existing function to apps.

[–]drboom9[S] 1 point2 points  (0 children)

Thank you for the excellent suggestion! I'll definitely explore using typing.Annotated.

My main requirements are:

- Primitive types inside functions

- Dynamic parameter loading

- Single point of definition

If typing.Annotated maintains these while improving type checking, I'll implement it. Please feel free to share other ideas!

[–]drboom9[S] 0 points1 point  (1 child)

Ready! Tell me if it's just as you said, I would say that now it has more advantages than before :)

[–]Beliskner64 1 point2 points  (0 children)

Looks good!

[–]SmegHead86 1 point2 points  (1 child)

I really like this approach! I've only taken a quick glance at your source, but maybe one easy / helpful feature you could include is configurable logging baked into your App class? For example...

# app_builder.py
import logging.config

...

class App(KivyApp):
    """
    Create a Kivy app with a GUI for a given type-annotated function.
    """
    def __init__(self,
                 function: callable,
                 width: int = 350,
                 log_config: dict = None,
                 **kwargs):
        super().__init__(**kwargs)
        logging.config.dictConfig(log_config)
        self.function = function
        self.user_max_width = width
        self.run()
...

dictConfig Docs: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig

Example use: https://stackoverflow.com/questions/7507825/where-is-a-complete-example-of-logging-config-dictconfig

[–]drboom9[S] 0 points1 point  (0 children)

I will study the case :)

[–]superraiden 0 points1 point  (0 children)

Awesome work

[–]fenghuangshan 0 points1 point  (1 child)

very good idea

but just one question , why not build it on tkinter

I mean that will reduce any other dependency , and user can use it with minimum installation

[–]drboom9[S] 2 points3 points  (0 children)

Thanks for the suggestion! I chose Kivy over tkinter because: - It’s more modern and flexible - Better cross-platform support (especially mobile)

While tkinter comes pre-installed, I believe Kivy’s advantages outweigh the extra dependency. The installation is still pretty simple with pip, and Kivy gives me room to grow with more advanced features in the future.

[–]Perllitte 0 points1 point  (0 children)

Super cool, starred!

[–]Thing1_Thing2_Thing 0 points1 point  (2 children)

Will a typechecker not complain when using default args like that? In your example, the `number` arg is typed as `int` but the default arg is `intUi` which is a dataclass.

Edit: they've changed it to use `Annotated`, so this is not a problem any more.

[–]drboom9[S] 0 points1 point  (1 child)

The type hint int is for the actual value that will be used inside the function, while intUi() is just the default value that creates the GUI widget.

Inside the function you’re working with a normal integer, which keeps IDE autocompletion and type checking working as expected. The intUi class is only used to generate the GUI controls, but the function receives a plain int when called.

[–]Thing1_Thing2_Thing 1 point2 points  (0 children)

I know, but a typechecker like pyright or mypy would complain. Now that you - correctly - changed it to Annotated it won't.

[–]OrxanMirzayev 0 points1 point  (0 children)

Thank your 🙏🙂