polyfactory icon indicating copy to clipboard operation
polyfactory copied to clipboard

Unexpected Collection Length Generation with `annotation_types.Len(1)`

Open Rub1kCube opened this issue 6 months ago • 2 comments

Problem Description

The main issue I've identified is that when using at.Len(1), the generator sometimes produces two values instead of one, contrary to what's stated in the documentation. I've located where this occurs in the code. If more than one value needs to be generated, the user should use the following options:

class Foo(BaseModel):
    bar: Annotated[list[int], at.Len(1)]

class FooFactory(Foo[Foo]):
    __randomize_collection_length__ = True
    __min_collection_length__ = 2
    __max_collection_length__ = 5

# Or

class FooFactory(Foo[Foo]):
    @classmethod
    def bar(cls) -> list[int]:
        return cls.__faker__.pylist(cls.__faker__.pyint(2, 5), value_types=[int])

Current Behavior

When at.Len(1) is specified, the collection can contain either 1 or 2 elements due to the current logic.

Proposed Solution

I propose the following fix:

min_items = abs(min_items if min_items is not None else (max_items or 0))
max_items = abs(max_items) if max_items is not None else max(min_items, 1)

Instead of the current implementation:

min_items = abs(min_items if min_items is not None else (max_items or 0))
max_items = abs(max_items if max_items is not None else min_items + 1)

Expected Results

With this change, we would observe the following behavior:

min_items=1, max_items=None → max_items=1
min_items=0, max_items=None → max_items=1
min_items=5, max_items=None → max_items=5
min_items=None, max_items=None → min_items=0, max_items=1

Alternative

If this behavior was intentional by design, I'm willing to document this behavior in the documentation instead.

MCVE

from typing import Annotated

import annotated_types as at

from pydantic import BaseModel

from polyfactory.factories.pydantic_factory import ModelFactory


class Foo(BaseModel):
    bar: Annotated[list[int], at.Len(1)]


class FooFactory(ModelFactory[Foo]):
    __random_seed__ = 4
    
print(ModelFactory.create_factory(Foo).build())

Release Version

2.21.0

Platform

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

Rub1kCube avatar Jun 03 '25 09:06 Rub1kCube

Proposed solution makes sense. I think constraints being a union of these makes sense which seems to be this behaviour

PRs welcome to fix

adhtruong avatar Jun 04 '25 19:06 adhtruong

@Rub1kCube @adhtruong Please take a look at the PR. I've created it against my fork.

priyankc avatar Jun 10 '25 04:06 priyankc