dry-python / returns

Make your functions return something meaningful, typed, and safe!
https://returns.rtfd.io
BSD 2-Clause "Simplified" License
3.58k stars 119 forks source link

Find out how pydantic validates function calls without calling them #875

Open sobolevn opened 3 years ago

sobolevn commented 3 years ago

We need to chec how pydantic validates function arguments without calling the function itself: https://pydantic-docs.helpmanual.io/usage/validation_decorator/#validate-without-calling-the-function

We need this because our @curry implementation relies on this logic. And right now it is very slow.

thepabloaguilar commented 3 years ago

I have a little explanation while I was walking through the code!

The class responsible for is ValidatedFunction! In its __init__ it process all parameters and types.

When we call validate(...) under the hood we're calling init_model_instance ant it calls two functions:

  1. build_values: That is responsible to inspect all the passed values to validate(...)
  2. model: This is not a function but a dynamically generated model

The model is created in create_model method, at the end we have a normal BaseModel but instead of class specifications we have function specifications! The validations occur in the validate_model function.

sobolevn commented 3 years ago

@thepabloaguilar can we adopt this / similar approach to speed up our @curry?

thepabloaguilar commented 3 years ago

Well, pydantic approach seems more complicated, I don't know if it'll be faster than our actual implementation.

I have something in mind for a long time, maybe we can use partial to build curry. Did you try it?? When we have a partial from a partial, both are merged:

>>> from functools import partial
>>> def f(a, b, c):
...     ...
...

>>> p1 = partial(f, 1)
>>> str(p1)
'functools.partial(<function f at 0x105bae160>, 1)'

>>> p2 = partial(p1, 2)
>>> str(p2)
'functools.partial(<function f at 0x105bae160>, 1, 2)'

# We have access to `args`, `kwargs` and `func`
>>> (p2.args, p2.keywords, p.func)
((1, 2), {}, <function a at 0x105b95ca0>)

I don't know if it's possible to build something using that approach, it's just thinking!

sobolevn commented 3 years ago

We need some logical flag to decide when to actually call a function: https://github.com/dry-python/returns/blob/master/returns/curry.py#L138-L139

thepabloaguilar commented 3 years ago

Hummmm. I want to make some tests around partial and pydantic approach!

Using partial we can use the inspect module to determine if we need to call:

>>> import inspect
# Consider `p1` and `p2` from my last example
>>> inspect.signature(p2)
<Signature (c)>
>>> inspect.signature(p1)
<Signature (b, c)>

>>> inspect.getfullargspec(p1)
FullArgSpec(args=['b', 'c'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
>>> inspect.getfullargspec(p2)
FullArgSpec(args=['c'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})

Pydantic: This for loop is responsible to get all of the function fields. To follow this approach we'll need to transform curry in a callable class instead of a function!