Unsolved 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.
-
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!