kalekundert / parametrize_from_file

Read unit test parameters from config files
MIT License
15 stars 3 forks source link

Converting input parameters to Pydantic base models #23

Open raminqaf opened 7 months ago

raminqaf commented 7 months ago

Hello! I am using your library to parametrize my tests! I am using Pydantic in my project and would like to use it to (de)serialize the input objects with it. My test case looks as follows:

test_object:
  - person:
      first_name: "John"
      last_name: "Doe"
    len: 4
class Person(BaseModel):
    first_name: str
    last_name: str

@pff.parametrize(schema=[pff.cast(person=Person)])
def test_object(person: Person, len: int):
    assert person.first_name == "John"
    assert len(person.first_name) == len

I don't know if I am using the schema and the cast correctly in this context, but I would like to convert the complex object into a base model. Alternatively, I can do this:

class Person(BaseModel):
    first_name: str
    last_name: str

@pff.parametrize
def test_object(person: dict, len: int):
    person_obj = Person(**person)
    assert person_obj.first_name == "John"
    assert len(person_obj.first_name) == len

which works, but it would be nice if the library could do the conversion on its own! Is there a way of doing this?

kalekundert commented 7 months ago

Yes, this is definitely something that you can do. The problem with the code you have is that pff.cast(person=Person) ends up calling Person({'first_name': 'John', 'last_name': 'Doe'}) (i.e. it passes the parameter read from the YAML file directly to the given callable), but pydantic constructors instead expect keyword arguments in the form of Person(first_name='John', last_name='Doe').

One easy way to solve this problem is to use Person.model_validate instead of Person. This factory function expects a single dictionary in the form that pff.cast() provides, so everything just works:

import parametrize_from_file as pff
from pydantic import BaseModel
import builtins

class Person(BaseModel):
    first_name: str
    last_name: str

@pff.parametrize(schema=[pff.cast(person=Person.model_validate)])
def test_object(person: Person, len: int):
    assert person.first_name == "John"
    assert builtins.len(person.first_name) == len

Another possible way to solve this problem is to use a lambda function to expand out the keyword arguments yourself. In this specific case, I think the Person.model_validate approach is better, but in other cases a lambda might make more sense:

@pff.parametrize(schema=[pff.cast(person=lambda x: Person(**x))])
def test_object(person: Person, len: int):
    ...

Let me know if you have any other questions, I'd be happy to help!

raminqaf commented 7 months ago

Nice! I used the first approach and it works! I am getting a warning from PyCharm on this part of the decorator: person=Person.model_validate

Expected type 'dict', got '(obj: Any, Any, strict: bool | None, from_attributes: bool | None, context: dict[str, Any] | None) -> Model' instead 
kalekundert commented 7 months ago

Unfortunately, I have no idea what's going on with that warning. This reminds me a bit of #20, which also involves PyCharm generating warnings where it shouldn't. pff.cast() doesn't have type-hints—maybe it would help if it did—but regardless it seems like PyCharm shouldn't be complaining about the type of an argument being passed to an untyped function...

raminqaf commented 7 months ago

Unfortunately, I have no idea what's going on with that warning. This reminds me a bit of #20, which also involves PyCharm generating warnings where it shouldn't. pff.cast() doesn't have type-hints—maybe it would help if it did—but regardless it seems like PyCharm shouldn't be complaining about the type of an argument being passed to an untyped function...

Maybe typing the function's input parameters and defining return types helps. For the warnings mentioned in #20 I changed this part of the code and managed to get rid of the parameter kwargs undefined warning

@_decorator_factory
-def parametrize(param_names, param_values, kwargs):

@_decorator_factory
+def parametrize(param_names, param_values, **kwargs):

The other warning, parameter param_values undefined, was still visible.

kalekundert commented 7 months ago

If adding ** in front of kwargs has any effect on the warning, that's definitely a sign that there's a bug in PyCharm. Adding type hints wouldn't help. The signature of the pff.parametize() as seen by end-users is determined by _decorator_factory() and has no relation to the signature of the parametrize() function itself. But PyCharm (or whatever linter it's using behind the scenes) doesn't seem smart enough to realize this.

Also, you should undo **kwargs change you made, because it 100% breaks the code. Specifically, it will prevent you from passing any arguments through to pytest.mark.parametrize.