"""Bridge between plain Python functions and pydantic v2 models.
Given a Python function, produce a pydantic input model whose fields mirror
the function's signature (names, annotations, defaults), and an "opyrator"-
style wrapper that takes a single pydantic model instance and dispatches
to the underlying function.
Useful for auto-generating forms, validators, and JSON-schema descriptions
of arbitrary callables — the core trick behind front's app-from-function
dispatch.
>>> from i2.tests.objects_for_testing import formula1
>>> pyd_input_model = func_to_pyd_input_model_cls(formula1)
>>> pyd_input_model.__name__
'formula1'
>>> from i2 import Sig
>>> Sig(formula1)
<Sig (w, /, x: float, y=1, *, z: int = 1)>
>>> Sig(pyd_input_model)
<Sig (*, w: Any, x: float, y: int = 1, z: int = 1) -> None>
>>> pyd_func = func_to_pyd_func(formula1)
>>> input_model_instance = pyd_input_model(w=1, x=2)
>>> input_model_instance
formula1(w=1, x=2.0, y=1, z=1)
>>> pyd_func(input_model_instance)
3.0
>>> formula1(1, x=2) # can't say w=1 because w is position only
3
"""
from typing import Any
from collections.abc import Callable
from functools import partial
from pydantic import create_model, BaseModel
from i2 import Sig, name_of_obj, empty_param_attr
from i2.wrapper import wrap, Ingress
from front.util import param_default
[docs]
def pyd_func_ingress_template(input_model_instance, wrapped_func_sig: Sig):
"""Turn a pydantic model instance into the ``(args, kwargs)`` of ``wrapped_func_sig``.
>>> from i2 import Sig
>>> from i2.tests.objects_for_testing import formula1
>>> model = func_to_pyd_input_model_cls(formula1)(w=1, x=2)
>>> pyd_func_ingress_template(model, Sig(formula1))
((1,), {'x': 2.0, 'y': 1, 'z': 1})
"""
kwargs = dict(input_model_instance)
args, kwargs = wrapped_func_sig.mk_args_and_kwargs(kwargs)
return args, kwargs
# TODO: Add the output model annotation
[docs]
def func_to_pyd_func(func: Callable, dflt_type=Any):
"""Get an 'opyrator' function from a python function: one taking a single pydantic model input.
The output model is not yet applied: the wrapped function returns what ``func`` returns.
"""
pyd_func_ingress = partial(pyd_func_ingress_template, wrapped_func_sig=Sig(func))
input_model = func_to_pyd_input_model_cls(func, dflt_type)
output_model = create_model(
"output_model", output_val=(Any, ...)
) # TODO: Work on this
# TODO: Inject annotations in pyd_func_ingress
return wrap(func, ingress=pyd_func_ingress)
[docs]
def func_to_pyd_model_specs(func: Callable, dflt_type=Any):
"""Helper function to get field info from python signature parameters.
Each spec is a ``(type, default)`` tuple suitable for pydantic v2's
``create_model``. For unannotated parameters the type is inferred from
the default value's type when a default is present, otherwise
``dflt_type`` (``Any`` by default) is used with ``...`` (required).
"""
for p in Sig(func).params:
default = param_default(p) # i2's NotSet sentinel counts as "no default"
if p.annotation is not empty_param_attr:
if default is not empty_param_attr:
yield p.name, (p.annotation, default)
else:
yield p.name, (p.annotation, ...)
else: # no annotations
if default is not empty_param_attr:
# pydantic v2 needs an explicit type; infer from the default
yield p.name, (type(default), default)
else:
yield p.name, (dflt_type, ...)
[docs]
def pydantic_egress(output):
"""Wrap ``output`` in an ``Output`` model with a single ``output_val`` field of its type.
>>> pydantic_egress(3)
Output(output_val=3)
"""
return_type = type(output)
mod = create_model("Output", output_val=return_type)
return mod(output_val=output)
[docs]
def pydantic_model_from_type(mytype, name="Output", field_name="result"):
"""Make a pydantic model with one required field ``field_name`` of type ``mytype``.
>>> Model = pydantic_model_from_type(int)
>>> Model(result=2)
Output(result=2)
"""
model = create_model(name, **{field_name: (mytype, ...)})
return model