Factory Boy stringifies models during factory initialisations with Trait params
The problem
When initialising a factory using a Trait param, Factory Boy handles it in the same manner as any other attribute error (ie the attribute does not exist on the model. It just so happens that the Trait params have a default value and the exception is therefore handled and the default value is returned.
The problem is that the AttributeError that is raised contains the stringified version of the model. It becomes especially problematic when the factory is a django model factory using a related object. Then the AttributeError contains the stringified version of the related model. A lot of django projects override the __str__ method of models to display meaningful information in django admin and this sometimes contains data of other related models.
This means during normal factory initialisations of django model factories using Trait params it will cause undesired database calls.
The exception is raised here: https://github.com/FactoryBoy/factory_boy/blob/9b256e48dd58ef7c97e3ddf09d972d1bb1d2df8e/factory/builder.py#L391-L393
The self.__values call calls the __repr__ method of DeclerationSet
https://github.com/FactoryBoy/factory_boy/blob/68feb45e182f9acccfde671b7ba0babb5bc7ce11/factory/builder.py#L133-L134
which returns itself as a dict, which in turns calls the __repr__ method of any object it has a reference to, which may be a django model
The exception is then handled here: https://github.com/FactoryBoy/factory_boy/blob/68feb45e182f9acccfde671b7ba0babb5bc7ce11/factory/declarations.py#L191-L195
Proposed solution
I suggest the error message is changed from:
raise AttributeError(
"The parameter %r is unknown. Evaluated attributes are %r, "
"definitions are %r." % (name, self.__values, self.__declarations))
to:
raise AttributeError(
"The parameter %r is unknown. "
"Definitions are %r." % (name, list(self.__declarations))
)
Then for this factory:
class FieldFactory(factory.Factory):
name = factory.fuzzy.FuzzyText(length=5)
value = factory.fuzzy.FuzzyText(length=5)
bar = factory.SelfAttribute("foo")
The error message goes from:
AttributeError: The parameter 'foo' is unknown. Evaluated attributes are {'name': 'PwkIJ', 'value': 'hcfJd'}, Definitions are <DeclarationSet: {'name': <factory.fuzzy.FuzzyText object at 0x13a34aa90>, 'value': <factory.fuzzy.FuzzyText object at 0x13a34b190>, 'bar': <SelfAttribute('foo', default=<class 'factory.declarations._UNSPECIFIED'>)>}>.
to:
AttributeError: The parameter 'foo' is unknown. Definitions are ['name', 'value', 'bar'].
Which is way more readable and useful. I don’t see any reason to include the detailed declarations and all the values already evaluated in the factory.
Another solution could be to identify Trait params and handle them differently instead of relying on try/except.
Extra notes
This was initially raised in the now closed issue #1009. I had put my discoveries and proposed solutions there (and a hacky workaround), but haven’t gotten any feedback there. So I decided to open a new issue to make sure it’s properly raised again.
I think this is a good change, i would do a slight modification:
AttributeError: The parameter 'foo' is unknown. Definitions are: name, value, bar.
or
AttributeError: The parameter 'foo' is unknown. Available attributes are: name, value, bar.
Basically the same, slightly more readable. Either way, its a change in the right direction.