Internals¶
Behind the scenes: steps performed when parsing a factory declaration, and when calling it.
This section will be based on the following factory declaration:
class UserFactory(factory.Factory):
class Meta:
model = User
class Params:
# Allow us to quickly enable staff/superuser flags
superuser = factory.Trait(
is_superuser=True,
is_staff=True,
)
# Meta parameter handling all 'enabled'-related fields
enabled = True
# Classic fields
username = factory.Faker('user_name')
full_name = factory.Faker('name')
creation_date = factory.fuzzy.FuzzyDateTime(
datetime.datetime(2000, 1, 1, tzinfo=UTC),
datetime.datetime(2015, 12, 31, 20, tzinfo=UTC)
)
# Conditional flags
is_active = factory.SelfAttribute('enabled')
deactivation_date = factory.Maybe(
'enabled',
None,
factory.fuzzy.FuzzyDateTime(
# factory.SelfAttribute('creation_date'),
datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=10),
datetime.datetime.now().replace(tzinfo=UTC) - datetime.timedelta(days=1),
),
)
# Related logs
creation_log = factory.RelatedFactory(
UserLogFactory, 'user',
action='create', timestamp=factory.SelfAttribute('user.creation_date'),
)
Parsing, Step 1: Metaclass and type declaration¶
Python parses the declaration and calls (thanks to the metaclass declaration):
factory.base.BaseFactory.__new__( 'UserFactory', (factory.Factory,), attributes, )
That metaclass removes
Meta
andParams
from the class attributes, then generate the actual factory class (according to standard Python rules)It initializes a
FactoryOptions
object, and links it to the class
Parsing, Step 2: adapting the class definition¶
- The
FactoryOptions
reads the options from theclass Meta
declaration - It finds a few specific pointer (loading the model class, finding the reference factory for the sequence counter, etc.)
- It copies declarations and parameters from parent classes
- It scans current class attributes (from
vars()
) to detect pre/post declarations - Declarations are split among pre-declarations and post-declarations (a raw value shadowing a post-declaration is seen as a post-declaration)
Note
A declaration for foo__bar
will be converted into parameter bar
for declaration foo
.
Instantiating, Step 1: Converging entrypoints¶
First, decide the strategy:
- If the entrypoint is specific to a strategy (
build()
,create_batch()
, ...), use it - If it is generic (
generate()
,Factory.__call__()
), use the strategy defined at theclass Meta
level
Then, we’ll pass the strategy and passed-in overrides to the _generate()
method.
Note
According to the project roadmap, a future version will use a _generate_batch`()
at its core instead.
A factory’s _generate()
function actually delegates to a StepBuilder()
object.
This object will carry the overall “build an object” context (strategy, depth, and possibly other).
Instantiating, Step 2: Preparing values¶
- The
StepBuilder
merges overrides with the class-level declarations - The sequence counter for this instance is initialized
- A
Resolver
is set up with all those declarations, and parses them in order; it will call each value’sevaluate()
method, including extra parameters. - If needed, the
Resolver
might recurse (through theStepBuilder
, e.g when encountering aSubFactory
.
Instantiating, Step 3: Building the object¶
- The
StepBuilder
fetches the attributes computed by theResolver
. - It applies renaming/adjustment rules
- It passes them to the
FactoryOptions.instantiate()
method, which forwards to the proper methods. - Post-declaration are applied (in declaration order)
Note
This document discusses implementation details; there is no guarantee that the described methods names and signatures will be kept as is.