FactoryBoy / factory_boy

A test fixtures replacement for Python
https://factoryboy.readthedocs.io/
MIT License
3.49k stars 392 forks source link

AttributeError when passing SelfAttribute to post_generation kwargs #789

Open citizen-stig opened 3 years ago

citizen-stig commented 3 years ago

Description

I have a model, that has a post_generation hook. I want to pass parameter to this post_generation hook from another factory, as SubFactory parameter to be a SelfAttribute, so these 2 models will be logically synced.

It might be that I'm doing something wrong, so advice is highly appreciated.

To Reproduce

When I create OrderItemFactory() it throws AttributeError: The parameter 'size' is unknown... Evaluated attributes are in fact from SubFactory's factory (product), not from top level factory (order item).

Model / Factory code

Here is simplified version

import factory
from factory import fuzzy
from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

SIZES = ('s', 'm', 'l')

# Models
class Product(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String())
    price = db.Column(db.Float(asdecimal=True), nullable=False)

class Inventory(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    size = db.Column(db.String(), nullable=True)
    quantity = db.Column(db.Integer())
    product_id = db.Column(db.Integer(), db.ForeignKey('product.id'), nullable=False)
    product = db.relationship(Product, backref=db.backref('inventory', lazy=True))

class OrderItem(db.Model):
    id = db.Column(db.Integer(), primary_key=True)
    size = db.Column(db.String(), nullable=True)
    quantity = db.Column(db.Integer(), nullable=False)
    product_id = db.Column(db.Integer(),
                           db.ForeignKey('product.id'),
                           nullable=False)
    product = db.relationship('Product')

# Factories
class BaseFactory(factory.alchemy.SQLAlchemyModelFactory):
    class Meta:
        abstract = True
        sqlalchemy_session = db.session
        sqlalchemy_session_persistence = 'flush'

class InventoryFactory(base_factory):
    class Meta:
        model = Inventory

    size = factory.fuzzy.FuzzyChoice(SIZES)
    quantity = factory.fuzzy.FuzzyInteger(1, 100)

class ProductFactory(BaseFactory):
    class Meta:
        model = Product

    name = factory.Sequence(lambda n: 'Product %d' % n)
    price = fuzzy.FuzzyDecimal(300, 4000)

    @factory.post_generation
    def add_inventory(self, create, how_many, **kwargs):
        for n in range(how_many):
            size = kwargs.get('size', set(SIZES).pop())
            InventoryFactory(product=self, size=size)

class OrderItemFactory(BaseFactory):
    class Meta:
        model = OrderItem

    size = factory.fuzzy.FuzzyChoice(SIZES)
    quantity = factory.fuzzy.FuzzyInteger(1, 10)
    product = factory.SubFactory(
        ProductFactory,
        # This works
        name=factory.SelfAttribute('..size'),
        add_inventory=1,
        # This works:
        # add_inventory__size='l',
        add_inventory__size=factory.SelfAttribute('..size'),
    )
The issue

Add a short description along with your code

_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/base.py:39: in __call__
    return cls.create(**kwargs)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/base.py:527: in create
    return cls._generate(enums.CREATE_STRATEGY, kwargs)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/alchemy.py:51: in _generate
    return super(SQLAlchemyModelFactory, cls)._generate(strategy, params)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/base.py:464: in _generate
    return step.build()
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:276: in build
    step.resolve(pre)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:217: in resolve
    self.attributes[field_name] = getattr(self.stub, field_name)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:382: in __getattr__
    extra=context,
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/declarations.py:315: in evaluate
    return self.generate(step, defaults)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/declarations.py:440: in generate
    return step.recurse(subfactory, params, force_sequence=force_sequence)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:234: in recurse
    return builder.build(parent_step=self, force_sequence=force_sequence)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:292: in build
    context=declaration.context,
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/declarations.py:35: in unroll_context
    return step.recurse(subfactory, context, force_sequence=step.sequence)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:234: in recurse
    return builder.build(parent_step=self, force_sequence=force_sequence)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:276: in build
    step.resolve(pre)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:217: in resolve
    self.attributes[field_name] = getattr(self.stub, field_name)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:382: in __getattr__
    extra=context,
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/declarations.py:153: in evaluate
    return deepgetattr(target, self.attribute_name, self.default)
../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/declarations.py:115: in deepgetattr
    return getattr(obj, name)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <Resolver for <BuildStep for <StepBuilder(<SQLAlchemyOptions for ProductFactory>, strategy='create')>>>
name = 'size'

    def __getattr__(self, name):
        """Retrieve an attribute's value.

        This will compute it if needed, unless it is already on the list of
        attributes being computed.
        """
        if name in self.__pending:
            raise errors.CyclicDefinitionError(
                "Cyclic lazy attribute definition for %r; cycle found in %r." %
                (name, self.__pending))
        elif name in self.__values:
            return self.__values[name]
        elif name in self.__declarations:
            declaration = self.__declarations[name]
            value = declaration.declaration
            if enums.get_builder_phase(value) == enums.BuilderPhase.ATTRIBUTE_RESOLUTION:
                self.__pending.append(name)
                try:
                    context = value.unroll_context(
                        instance=self,
                        step=self.__step,
                        context=declaration.context,
                    )

                    value = value.evaluate(
                        instance=self,
                        step=self.__step,
                        extra=context,
                    )
                finally:
                    last = self.__pending.pop()
                assert name == last

            self.__values[name] = value
            return value
        else:
            raise AttributeError(
                "The parameter %r is unknown. Evaluated attributes are %r, "
>               "definitions are %r." % (name, self.__values, self.__declarations))
E           AttributeError: The parameter 'size' is unknown. Evaluated attributes are {'name': 'l', 'price': Decimal('3632.62')}, definitions are <DeclarationSet: {'name': <SelfAttribute('size', default=<class 'factory.declarations._UNSPECIFIED'>)>, 'price': <factory.fuzzy.FuzzyDecimal object at 0x11251eeb8>}>.

../../../.local/share/virtualenvs/merch-x5SnXW63/lib/python3.7/site-packages/factory/builder.py:393: AttributeError

Notes

Factory boy version 3.1.0

niksite commented 2 years ago

Does replacing of factory.SelfAttribute('..size') with factory.SelfAttribute('...size') help?