Open rbarrois opened 7 years ago
Other issues with the current design:
RelatedFactory
If the factory has a related factory (e.g profile = factory.RelatedFactory(ProfileFactory, 'user')
, what should happen when the user calls UserFactory(profile=a_profile)
:
_after_postgeneration
hook?PostGenerationMethodCall
When a method accepts several positional args, how should we handle them?
For instance, with a field password = factory.PostGenerationMethodCall('set_password', 'pwd123', 'pbkdf2')
:
UserFactory(password='test')
or UserFactory(password=('test', 'pbkdf2'))
?After a couple of discussions with factory_boy users, the idea is to go as follow:
PostGenerationMethodCall
/ @post_generation
: a Django password setter (see #366) and a simpler way to manage "many-to-many related objects"The chosen options from the above list of choices are:
UserFactory(password='test')
syntaxRelatedFactory
behaviorPostGenerationMethodCall
(improves readability anyway)UserFactory(groups=factory.DISABLE_POSTGEN)
)How should we handle post-declaration/pre-declaration relations?
With the above factory, when the user calls UserFactory(password=factory.SelfAttribute('username'))
, should we:
User(username='john', password='john')
; not what they would expectUserFactory
, and pass the generated value to the post-declarationPostGenerationMethodCall
If we go with option 3, does this work with other post-generation declarations?
Not sure this will help you but as a user and consumer of factories written by others, I would have typed:
user = UserFactory()
user.set_unusable_password()
in my test code.
I didn't know about PostGenerationMethodCall
(or never paid attention) but this writing is straightforward to read and doesn't need to remember all of the factory-boy API for every case.
@bors-ltd yes, it works well ;)
However, it doesn't scale if you want to write, for instance, a DisabledUserFactory
If I can throw in my opinion: I'd like to be able to use a SubFactory and/or RelatedFactory in such a way that when I pass a value to the factory (e.g. UserFactory(profile=a_profile)
) the SubFactory and/or the RelatedFactory is not called at all, and I won't have to override it in the post_generation or anywhere else.
I don't see a reason why it should be called at all if I'm passing the value. This is creating a situation where extra unintended objects are being written to the db.
You can support passing a raw_password to factory_boy like so:
class UserFactory(Factory):
password = factory.PostGeneration(lambda user, create, extracted: user.set_password(extracted))
The create
arg is irrelevant here, that simply specifies whether you're using the build or create strategy – set_password
doesn't save
so it doesn't matter which
The extracted
arg is what was passed to the factory call – by default this is None
so you end up with an unusable password – which is probably fine as a default
In [1]: UserFactory().has_usable_password()
Out[1]: False
In [2]: UserFactory(password='test').has_usable_password()
Out[2]: True
In [3]: UserFactory(password='test').check_password('test')
Out[3]: True
In [4]: UserFactory(password='test').password
Out[4]: 'argon2$argon2i$v=19$m=512,t=2,p=2$blahblahblah'
To clarify, the reason you have to set the password in PostGeneration
is because up until that point (e.g. when LazyAttribute
's are evaluated) – factory_boy is working with a user stub – not a user object – so the set_password
method is not available – the stub is later used to build the model instance – this design is to support building things other than model instances (like dicts)
As part of an ongoing refactor of factory_boy's core engine (which should remove lots of quirks and asymmetry between different kinds of declarations), we need to decide how to handle a specific case with post-generation declaration.
Context
Suppose we have the following factory:
With the current code, a user may do:
On the other hand, when typing:
One would expect that this changes the password generation to call
set_unusable_password
, instead of trying to use thePostGenerationMethodCall
object as a password (unlikely to work :wink:).Issue
This means that post-generation declarations aren't handled as pre-generation are:
This means that:
UserFactory(password='!')
without going through any post-generation hash computation).Options
I see a few options to address this point:
UserFactory(password__='test')
UserFactory(password__cleartext='test')