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:

  1. Subclass Factory
  2. Define _model which is the type of the model to instantiate.
  3. Define _model_builder to point to the model builder you created.
  4. 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:

  1. Instantiate your factory. This allows you to pass configuration, if needed, to the factory, as well as to hold instante state for the schemas.
  2. Call the factory API to realize a schema. In the above example, we’re creating a model with the basic 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.