litestar-org/polyfactory

Bug: Calling build of a factory with parameter for an optional nested field does not work

skallfass opened this issue · 2 comments

Description

In this example I expect the assertion error to never be raised, but it is.
It seems as the argument for nested in this case is not used on build.
I think the problem is related to the kwargs used in https://github.com/litestar-org/polyfactory/blob/main/polyfactory/factories/base.py#L729 as they are mutable and modified in functions called by this function.
If kwargs are not used directly but as kwargs.copy(), the problem does not occur.

URL to code causing the issue

https://github.com/litestar-org/polyfactory/blob/main/polyfactory/factories/base.py#L729

MCVE

from typing import Optional, TypedDict

from polyfactory.factories.typed_dict_factory import TypedDictFactory


class NestedStructure(TypedDict):
    field_a: int
    field_b: str


class MainStructure(TypedDict):
    name: str
    nested: Optional[NestedStructure]


class MainStructureFactory(TypedDictFactory[MainStructure]):
    __model__ = MainStructure


for attempt in range(10):
    example = MainStructureFactory.build(nested=NestedStructure(field_a=1, field_b="2"))
    assert example["nested"], f"attempt {attempt} failed"

Steps to reproduce

1. Run the example code
2. See assertion error

Screenshots

No response

Logs

Traceback (most recent call last):
  File ".poly_check.py", line 23, in <module>
    assert example["nested"], f"attempt {attempt} failed"
           │                             └ 4
           └ {'nested': None, 'name': 'JtCbpukJvunqdfIKCZis'}
AssertionError: attempt 4 failed

Release Version

2.8.0

Platform

  • Linux
  • Mac
  • Windows
  • Other (Please specify in the description above)

Funding

  • If you would like to see an issue prioritized, make a pledge towards it!
  • We receive the pledge once the issue is completed & verified
Fund with Polar
guacs commented

Hi. Thanks for reporting this! This is indeed a bug. It's due to the fact that it's not possible to figure out that an instance of a dictionary is an instance of a TypedDict. This causes extract_field_build_parameters to return the value for nested even though it shouldn't.

There are two workarounds for this:

  1. Don't use a TypedDict for your nested structure but instead use a dataclass or pydantic model etc. This is an easy option if you don't need to use a TypedDict.

  2. If you need to absolutely use a TypedDict, you could override the build method on the factory with something like this:

class NestedStructure(TypedDict):
    field_a: int
    field_b: str

class MainStructure(TypedDict):
    name: str
    nested: Optional[NestedStructure]


class MainStructureFactory(TypedDictFactory[MainStructure]):
    __model__ = MainStructure

    _sentinel = object()
    
    @classmethod
    def build(cls, **kwargs: Any) -> MainStructure:

        nested = kwargs.pop("nested", cls._sentinel)
        instance = super().build(**kwargs)

        if nested is not cls._sentinel:
            instance["nested"] = nested

        return instance
guacs commented

I'm closing this since I don't think there's an obvious fix for this due to how TypedDict works.