Factories¶
Factories are the bread and butter of PyFactory. Once you have a model builder for your models, you’re ready to write factories. The key terms to understand are the following:
- factory - The class which can be used to instantiate models.
- schema - A type of model to instantiate from a single factory. Schemas define the structure of the model which is built.
Another way to think of it that a factory contains many schemas with which to build models.
Creating a Factory¶
Creating the Factory Class¶
To define a factory:
- Subclass
Factory
- Define
_model
which is the type of the model to instantiate. - Define
_model_builder
to point to the model builder you created. - Define one or many schemas.
Here is an example factory:
from pyfactory import Factory
class MyFactory(Factory):
_model = MyModel
_model_builder = MyModelBuilder
# Define schemas here. This will be explained later.
Schemas¶
Schemas define the structure of a created model. A factory class can contain many schemas and therefore know how to instantiate models with many different attributes.
A schema is an instance method which returns a dictionary of attributes,
and the method must be decorated with
@schema()
. This dictionary
is then used to instantiate the model.
An example schema, for a hypothetical User
model:
@schema()
def basic(self):
return {
"first_name": "John",
"last_name": "Doe"
}
Note that self
points to an instance of your
Factory
, so you can call
any methods on it. The uses of this are shown later for
schema inheritance.
Using a Factory¶
Once the factory class is defined, using it is simple, since it has a very simple API. Here is an example of using the factory we created above:
user = MyFactory().create("basic")
The basic steps are:
- Instantiate your factory. This allows you to pass configuration, if needed, to the factory, as well as to hold instante state for the schemas.
- Call the
factory API
to realize a schema. In the above example, we’re creating a model with thebasic
schema.
There are three main ways to realize a schema:
attributes
- This will return the raw dictionary of the schema. This doesn’t invoke your model builder at all.build
- This will return an instance of your model, but will not persist it to any backing store.create
- This will return an instance of your model which is persisted to the backing store after being created.
Note that depending on your model builder and type of models you’re
creating, build
and create
may not be different at all. The
two differences are provided for convenience.
Overriding Schema Attributes¶
While schemas and provide a common skeleton and simple way to quickly
build out records, it is very common that you want to override one
or more fields of the record. You can do this very easily by passing
additional keyword arguments to any of the schema realization methods.
For example, for the user above, if we wanted to override the
first_name
field, we can do so easily:
user = UserFactory().create("basic", first_name="Bob")
print user.first_name # => "Bob"
print user.last_name # => "Doe"
This can be done with any field and any number of them.
Schema Inheritance¶
It is often the case that some sort of “schema inheritance” is
necessary. For example, a User
might be the same in every way
except for a type
field noting whether they’re an admin, user,
guest, etc. In such a case, creating one shared schema and reusing
it with subtle differences is the way to go:
@schema()
def base(self):
return {
"name": "John Doe"
}
@schema()
def admin(self):
base = self.schema("base")
base["type"] = "admin"
return base
@schema()
def user(self):
base = self.schema("base")
base["type"] = "user"
return base
This way, all our shared attributes are in the base
schema,
and the other schemas modify it in a subtle way.
Note
The schema
method should be used instead of attributes
.
attributes
will resolve any special fields, and this usually
is not the behavior you would like because you want overrides to
take effect prior to any special field resolution. schema
will
return the raw schema dictionary.
Factory Inheritance¶
Although slightly more rare, it is sometimes useful to have factory inheritance, where one factory inherits from another. This is the same as any other class inheritance in Python. The only difference is when you want to create a schema of the same name, but also want to use the attributes of the parent schema of the same name. For example, instead of using schema inheritance above, we could’ve used factory inheritance. Example:
class MySubFactory(MyFactory):
@schema()
def base(self):
base = super(MySubFactory, self).attributes("base")
base["email"] = "myemail@domain.com"
return base
Hopefully there is nothing surprising here. The only thing is that
we can’t simply super and call base
since schemas are treated
differently than normal instance methods. Instad, we use the fact
that we’re a factory to ask for the attributes of the parent’s
“base” schema, and use that.