factory_boy icon indicating copy to clipboard operation
factory_boy copied to clipboard

factory.SelfAttribute not evaluated when passed as list?

Open Kagee opened this issue 8 months ago • 10 comments

Description

I am not sure if this is me using factory_boy wrong, or a bug. When i use a factory.SelfAttribute to pass a attribue to a provider it is evaluated if i pass it directly, but not if passed in a list (alone or with more factory.SelfAttribute's)

Model / Factory code
    # Money object generated: ¥2,748
    subtotal = factory.Faker("djmoney", currency=_currency)

    # money is Money object: ¥2,748 / <class 'djmoney.money.Money'>
    tax = factory.Faker(
        "djmoney",
        money=factory.SelfAttribute("..subtotal"),
        multiplier=0.25,
    )
    # Money is not list of Money objects as expected: 
    # [<SelfAttribute('subtotal', default=<class 'factory.declarations._UNSPECIFIED'>)>]
    # Also, the SelfAttribute says "subtotal" not "..subtotal" anymore?
    total = factory.Faker(
        "djmoney",
        money=[
            factory.SelfAttribute("..subtotal"),
            # factory.SelfAttribute("..tax"), # Same problem with one or more list items
        ],
    )

Notes

I think the custom provider i am using is irellevant, but its code can be found here: https://gitlab.com/Kagee/homelab-organizer/-/blob/develop/hlo/factories/providers/moneyprovider.py

Kagee avatar May 01 '25 21:05 Kagee

The same happens if i use factory.LazyAttribute(lambda o: o.factory_parent.subtotal), but then i get a list of LazyAttribute ofc.

Kagee avatar May 01 '25 21:05 Kagee

Can you show the full method/class? The example you provided is a bit incomplete.

kingbuzzman avatar May 01 '25 21:05 kingbuzzman

@kingbuzzman The full method/class of what exactly? The whole (subject to change) factory can be seen here https://gitlab.com/Kagee/homelab-organizer/-/blob/develop/hlo/factories/order.py

With help from the Django discord, i rubberducket my way to this code, that worked. If this is how it is supposed to be done, i feel it was not obvious from the documentation.

    total = factory.Faker(
        "djmoney",
        money=factory.List(
            [
                factory.SelfAttribute("..factory_parent.subtotal"),
                factory.SelfAttribute("..factory_parent.tax"),
            ],
        ),
    )

Kagee avatar May 01 '25 21:05 Kagee

I guess the reason I'm confused it's because factory.Faker is only ever used inside a Factory and never alone. Example

kingbuzzman avatar May 01 '25 21:05 kingbuzzman

Yes, the DjangoModelFactory i linked to https://gitlab.com/Kagee/homelab-organizer/-/blob/develop/hlo/factories/order.py

Kagee avatar May 01 '25 21:05 Kagee

It's hard to reproduce your full setup, but i got "something" working, and I'm getting what looks to be correct. Can you provide a full example of the issue please?

Code (click here to expand!!).

(copy and paste friendly code that can go straight into ipython with no fuss-no muss)

import factory
from decimal import Decimal
import logging
logger = logging.getLogger(__name__)
from django.conf import settings
from faker.providers import BaseProvider
Money = Decimal

class MoneyProvider(BaseProvider):
    # settings.CURRENCIES
    def djmoney(
        self,
        text: str | None = None,
        currency: str | None = None,
        multiplier: Decimal = 1,
        money: Money | list[Money] | None = None,
    ) -> Money:
        if money:
            # Use the input currency
            if isinstance(money, Money):
                logger.debug("Money is Money: %s / %s", money, type(money))
                new_money = Money(
                    money.amount * Decimal(multiplier),
                )
            else:
                logger.debug("Money is not Money: %s", money)
                new_money = Money(0)
                for m in money:
                    new_money += m
                new_money = new_money * Decimal(multiplier)
            logger.debug("Money: %s, New money: %s", money, new_money)
            return new_money
        if not currency:
            currency = self.random_choices(settings.CURRENCIES, length=1)[0]
        else:
            pass
        if not text:
            # For USD and EUR
            text = "@%#,##"
            if currency == "NOK":
                # 10 NOK ~1 USD, add a digit
                text = "@%##,##"
            if currency == "JPY":
                # 100 NOK ~.7 USD, add two digits and no decimals
                text = "@%###"
        m = Money(
            Decimal(self.numerify(text=text).replace(",", "."))
            * Decimal(multiplier),
        )
        logger.debug("Money returned: %s", m)
        return m

factory.Faker.add_provider(MoneyProvider)

class OrderFactory(factory.Factory):
    class Meta:
        model = dict

    subtotal = 5
    tax = 5
    total = factory.Faker(
        "djmoney",
        money=factory.List(
            [
                factory.SelfAttribute("..factory_parent.subtotal"),
                factory.SelfAttribute("..factory_parent.tax"),
            ],
        ),
    )

output

>>> OrderFactory()
{'subtotal': 5, 'tax': 5, 'total': Decimal('10')}

gets summed up and multiplied by 1

kingbuzzman avatar May 01 '25 22:05 kingbuzzman

Yes, i also got it to work when using factory.List and ..factory_parent.subtotal.

I am still not sure why i had to change the query for subtotal, but i would suggest adding "sending a list of SelfAtrribue/LazyAttribute as argument to a provider" be added to the examples in docs.

Kagee avatar May 02 '25 08:05 Kagee

PRs welcome! Would this close this issue?

kingbuzzman avatar May 02 '25 10:05 kingbuzzman

Where in the docs would this make the most sense?

Kagee avatar May 02 '25 10:05 Kagee

I'm going to be honest; I'm not entirely sure what the issue is..

@rbarrois can you provide some guidance?

kingbuzzman avatar May 02 '25 16:05 kingbuzzman