litestar-org / polyfactory

Simple and powerful factories for mock data generation
https://polyfactory.litestar.dev/
MIT License
997 stars 78 forks source link

Bug: Pydantic ModelFactory coverage() function does not work with types including None #444

Closed c-pletinckx closed 9 months ago

c-pletinckx commented 9 months ago

Description

I have a simple pydantic model:

class SimpleModel(BaseModel):
    a: int
    b: float
    c: str
    d: Optional[bool]

I want to test a function that accepts an instance of this model as input. The actual function implementation is not important here:

def handle_input(input_instance: SimpleModel) -> None:
    pass

In order to do so, I would like to use the coverage() function of pydantic ModelFactory as such:

def test_handle_input(self):
    model_generator = SimpleModelFactory.coverage()

    for model in model_generator:
        handle_input(model)

This test, when called, returns a TypeError exception at line for model in model_generator.

However, it works very well with a pydantic model defined as follows:

class SimpleModel(BaseModel):
    a: int
    b: float
    c: str
    d: Union[bool, int]

I expected ModelFactory to work for all cases where None is a valid value (e.g. Optional[int], Union[NoneType, int, float], etc.) but it seems that, for now, either it is not a feature polyfactory wants to handle or there is a bug in the code base.

URL to code causing the issue

No response

MCVE

from typing import Optional

from polyfactory.factories.pydantic_factory import ModelFactory
from pydantic import BaseModel

class SimpleModel(BaseModel):
    a: int
    b: float
    c: str
    d: Optional[bool]

class SimpleModelFactory(ModelFactory[SimpleModel]):
    __model__ = SimpleModel

for model in SimpleModelFactory.coverage():
    print(model)

Steps to reproduce

1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

Screenshots

"In the format of: ![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)"

Logs

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)

scratch.ipynb Cell 4 line 2
     20 class SimpleModelFactory(ModelFactory[SimpleModel]):
     21     __model__ = SimpleModel
---> 23 for model in SimpleModelFactory.coverage():
     24     print(model)

File polyfactory/factories/base.py:928, in BaseFactory.coverage(cls, **kwargs)
    919 @classmethod
    920 def coverage(cls, **kwargs: Any) -> abc.Iterator[T]:
    921     """Build a batch of the factory's Meta.model will full coverage of the sub-types of the model.
    922 
    923     :param kwargs: Any kwargs. If field_meta names are set in kwargs, their values will be used.
   (...)
    926 
    927     """
--> 928     for data in cls.process_kwargs_coverage(**kwargs):
    929         instance = cls.__model__(**data)
    930         yield cast("T", instance)

File polyfactory/factories/base.py:886, in BaseFactory.process_kwargs_coverage(cls, **kwargs)
    880             result[field_meta.name] = cls._handle_factory_field_coverage(
    881                 field_value=field_value,
    882                 field_build_parameters=field_build_parameters,
    883             )
    884             continue
--> 886         result[field_meta.name] = CoverageContainer(
    887             cls.get_field_value_coverage(field_meta, field_build_parameters=field_build_parameters),
    888         )
    890 for resolved in resolve_kwargs_coverage(result):
    891     for field_name, post_generator in generate_post.items():

File polyfactory/utils/model_coverage.py:45, in CoverageContainer.__init__(self, instances)
     43 def __init__(self, instances: Iterable[T]) -> None:
     44     self._pos = 0
---> 45     self._instances = list(instances)
     46     if not self._instances:
     47         msg = "CoverageContainer must have at least one instance"

File polyfactory/factories/base.py:721, in BaseFactory.get_field_value_coverage(cls, field_meta, field_build_parameters)
    718 elif is_any(unwrapped_annotation) or isinstance(unwrapped_annotation, TypeVar):
    719     yield create_random_string(cls.__random__, min_length=1, max_length=10)
--> 721 elif provider := cls.get_provider_map().get(unwrapped_annotation):
    722     yield CoverageContainerCallable(provider)
    724 elif callable(unwrapped_annotation):
    725     # if value is a callable we can try to naively call it.
    726     # this will work for callables that do not require any parameters passed

TypeError: unhashable type: 'list'

Release Version

Platform


[!NOTE]
While we are open for sponsoring on GitHub Sponsors and OpenCollective, we also utilize Polar.sh to engage in pledge-based sponsorship.

Check out all issues funded or available for funding on our Polar.sh Litestar dashboard

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
  • This, along with engagement in the community, helps us know which features are a priority to our users.

Fund with Polar

guacs commented 9 months ago

@c-pletinckx, this was fixed in #440 but it just hasn't been released yet. I'll do a release today or tomorrow.

guacs commented 8 months ago

@c-pletinckx so sorry for the delay in the release! v2.13.0 has been released which includes the fix for this.