Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converting input parameters to Pydantic base models #23

Open
raminqaf opened this issue Feb 5, 2024 · 5 comments
Open

Converting input parameters to Pydantic base models #23

raminqaf opened this issue Feb 5, 2024 · 5 comments

Comments

@raminqaf
Copy link

raminqaf commented Feb 5, 2024

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
Copy link
Owner

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
Copy link
Author

raminqaf commented Feb 6, 2024

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
Copy link
Owner

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
Copy link
Author

raminqaf commented Feb 7, 2024

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
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants