polyfactory icon indicating copy to clipboard operation
polyfactory copied to clipboard

Bug: Infinite recursion error with discriminated unions

Open judahrand opened this issue 2 years ago • 4 comments

Description

This seems to occur when a Discriminated Union model has a Discriminated Union field. The first level of this problem was fixed in #76 but it is still broken for the next level down.

URL to code causing the issue

No response

MCVE

from typing import Literal, Union

from typing_extensions import Annotated

from pydantic import BaseModel, Field

class BlackCat(BaseModel):
    pet_type: Literal['cat']
    color: Literal['black']
    black_name: str
    pet: 'Pet'

class WhiteCat(BaseModel):
    pet_type: Literal['cat']
    color: Literal['white']
    white_name: str
    pets: 'Pet'

Cat = Annotated[Union[BlackCat, WhiteCat], Field(discriminator='color')]

class Dog(BaseModel):
    pet_type: Literal['dog']
    name: str
    pet: 'Pet'

Pet = Annotated[Union[Cat, Dog], Field(discriminator='pet_type')]
BlackCat.update_forward_refs()
WhiteCat.update_forward_refs()
Dog.update_forward_refs()

class Model(BaseModel):
    pet: Pet
    n: int

from polyfactory.factories.pydantic_factory import ModelFactory

class MyModelFactory(ModelFactory):
    __model__ = Model


# This will create a max recursion error
obj = MyModelFactory.build()

print(obj)

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

No response

Litestar Version

polyfactory==2.0.1

Platform

  • [ ] Linux
  • [ ] Mac
  • [ ] Windows
  • [ ] Other (Please specify in the description above)
Fund with Polar

judahrand avatar May 02 '23 09:05 judahrand

I've got some fairly complex models and haven't identified the root cause of this, but I'm also getting infinite recursion errors with pydantic factories. The models themselves definitely don't recurse though.

Python 3.10, pydantic 1.10.7, polyfactory 2.1.0

Stack trace if it's useful:

.../test_ThingView.py:24: in <listcomp>
    things = [thing_factory.build() for i in range(10)]
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:166: in build
    processed_kwargs = cls.process_kwargs(**kwargs)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:681: in process_kwargs
    result[field_meta.name] = cls.get_field_value(field_meta, field_build_parameters=field_build_parameters)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:600: in get_field_value
    return handle_complex_type(field_meta=field_meta, factory=cls)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/value_generators/complex_types.py:72: in handle_complex_type
    return factory.get_field_value(field_meta)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:600: in get_field_value
    return handle_complex_type(field_meta=field_meta, factory=cls)
E   RecursionError: maximum recursion depth exceeded in comparison
!!! Recursion detected (same locals & position)
=============================================================================== short test summary info ===============================================================================
FAILED .../tests/test_ThingView.py::test_ThingView_does_right_thing[2] - RecursionError: maximum recursion depth exceeded in comparison
================================================================================== 1 failed in 1.34s ============================================================================

aliceh75 avatar May 17 '23 12:05 aliceh75

I've got some fairly complex models and haven't identified the root cause of this, but I'm also getting infinite recursion errors with pydantic factories. The models themselves definitely don't recurse though.

Python 3.10, pydantic 1.10.7, polyfactory 2.1.0

Stack trace if it's useful:

.../test_ThingView.py:24: in <listcomp>
    things = [thing_factory.build() for i in range(10)]
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/pydantic_factory.py:166: in build
    processed_kwargs = cls.process_kwargs(**kwargs)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:681: in process_kwargs
    result[field_meta.name] = cls.get_field_value(field_meta, field_build_parameters=field_build_parameters)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:600: in get_field_value
    return handle_complex_type(field_meta=field_meta, factory=cls)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/value_generators/complex_types.py:72: in handle_complex_type
    return factory.get_field_value(field_meta)
/var/virtualenv/lib/python3.10/site-packages/polyfactory/factories/base.py:600: in get_field_value
    return handle_complex_type(field_meta=field_meta, factory=cls)
E   RecursionError: maximum recursion depth exceeded in comparison
!!! Recursion detected (same locals & position)
=============================================================================== short test summary info ===============================================================================
FAILED .../tests/test_ThingView.py::test_ThingView_does_right_thing[2] - RecursionError: maximum recursion depth exceeded in comparison
================================================================================== 1 failed in 1.34s ============================================================================

Do you have an MCVE? Is it also related to nested discriminated unions?

Goldziher avatar May 17 '23 12:05 Goldziher

Do you have an MCVE?

Not yet, models are a bit complex and I'll have to slowly remove things until I can identify source of the problem.

aliceh75 avatar May 17 '23 14:05 aliceh75

Not sure if it's the same issue but I ran into RecursionError with recursive types. MCVE:

from typing import TypeAlias, Literal

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


Unit: TypeAlias = "UnitProduct | UnitRatio | AtomicUnit"


class UnitProduct(BaseModel):
    """A product of two units"""

    unit1: Unit
    unit2: Unit


class UnitRatio(BaseModel):
    """A ratio of two units"""

    numerator: Unit
    denominator: Unit


class AtomicUnit(BaseModel):
    """A base unit that cannot be further reduced"""

    unit: Literal["m", "s", "kg", "A", "K", "mol", "cd"]


class Quantity(BaseModel):
    value: float
    unit: Unit


class QuantityFactory(ModelFactory):
    __model__ = Quantity

    @classmethod
    def value(cls):
        return round(cls.__random__.uniform(0, 10), 2)


if __name__ == '__main__':
    print(QuantityFactory.build())

gsakkis avatar Nov 16 '23 15:11 gsakkis

This may fixed now with the changes from https://github.com/litestar-org/polyfactory/pull/468.

  • The original MVC has no way to be constructed easily as fully recursive. If a type in one of the models is changed to Pet | None then this works.
  • https://github.com/litestar-org/polyfactory/issues/205#issuecomment-1814654295 works with main with no changes

adhtruong avatar Mar 10 '24 13:03 adhtruong

@adhtruong thanks for pointing this out. I think you're right that the original MCVE is always going to result in an infinite recursion unless we have a base case we can use.

Also, @adhtruong could you hop in on the Discord channel?

vkcku avatar Mar 13 '24 09:03 vkcku