Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

PydanticSlot Decorator



  • Hi everyone, after playing around with how to make an app easy for others to contribute to, I (re)discovered Pydantic and was able to make a decorator that automates the de-serialization, validation, and serialization in that order, when using Qt for Python. The need arose when attempting to formalize the syntax for sending json round trip from the QML front end to the python backend.

    Now instead of every class having to have its own hooks and validation code, I can just use Pydantic models as shown below, which was inspired by the FastAPI tutorials I played with in the past.

    class GCode(BaseModel, extra=Extra.forbid):
        text: str
    
    
    class ToolTable(BaseModel, extra=Extra.forbid):
        tools: list[str]
    
    
    class ToolTableResponse(Response):
        tool_table: Union[list, None]
    
    
    class QMLToolTableGenerator(QObject):
        """Bridge between the tool_table_generator module
        and the qml front end."""
    
        def __init__(self):
            super().__init__()
    
        @PydanticSlot(model=GCode)
        def generate(self, payload: GCode) -> Response:
            """Generates a tool table from gcode
            text."""
    
            try:
                tool_table = ttg.generate(payload.text)  # generate the tool table
                if not tool_table:
                    raise ValueError("No tools found")
            except Exception as e:
                r = ToolTableResponse(status=False,
                                      message=str(e))
            else:
                r = ToolTableResponse(status=True,
                                      message="tool table generated successfully",
                                      tool_table=tool_table)
            return r
    

    The resulting code is now so easy to work with and understand that I decided to share the source code of the decorator as shown below.

    def PydanticSlot(model=None):
        """The PydanticSlot acts as a serialization layer between pure Python
        functions that take Pydantic Models as arguments, and a QML front end.
        The advantages are:
        * More readable code, with arguments being Pydantic Models,
          the "cognitave overhead" is reduced greatly.
        * Clearly defined endpoints.
        * Runtime validation.
        * Allows developers to determine if the problem with a function
          call is the arguments passed in, or the function implementation
          its self.
        """
    
        def inner(func):                                # Grab the functions
            @Slot(str, name=func.__name__, result=str)  # PySide string interface wrapper
            @functools.wraps(func)                      # Keeps our stack trace intact
            def wrapper(*args, **kwargs):               # The serialization is performed in the wrapper
                # check if in_model is provided
                if model is not None:
                    try:
                        # check if method belongs to a PySide class
                        if isinstance(args[0], QObject):
                            self, *payload = args             # seperate into self & args
                            item = model.parse_raw(*payload)  # de-serialize the payload
                            args = (self, item)               # regenerate the argument tuple
                        else:
                            args = model.parse_raw(*args)     # de-serialize the payload
    
                    except Exception as e:
                        error_message = "\n".join([
                            f"failed on call to {func.__code__.co_name}",  # which function was called
                            f"from module {func.__module__}",              # which module it belongs to
                            "with the following arguments:",
                            "\t\n".join([str(a) for a in args]),           # which arguments were passed in
                            "with the following error:",
                            str(e)                                         # the resulting error
                        ])
                        return Response(status=False,                      # return json response
                                        message=error_message).json()
    
                    else:
                        return func(*args, **kwargs).json()                # return the json response
            return wrapper                                     # return the wrapper
        return inner                                      # return the decorator
    

    I know it could be ironed out further and made more general, so I would love some feedback on this, and it was so helpful I was wondering if there was any interest in formalizing it and adding official support for Pydantic models into Qt for Python.

    Thanks in advance for any advice and/or critiques.



  • Thanks for sharing.

    Please post source code as text rather than a screen capture from an editor. Posting as a capture forces readers who want to use or quote the code to retype it rather than copying and pasting. It breaks text wrapping and font resizing on mobile devices or other unusual screen sizes. Screen readers and high contrast themes for sight impaired users are unlikely to work.


  • Lifetime Qt Champion

    Hi,

    Looks nice, thanks !

    As @jeremy_k wrote, the text version would be really nice.

    One thing I would modify is status. It's usually a name that if find associated with a code or an enumeration rather than a Boolean value even if the value has only two possibilities. Maybe something like "is_valid" might better fit its meaning.



  • @SGaist thank you for the advice, I think that I will take you up on that!