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 :py:class:`Factory <pyfactory.factory.Factory>`
2. Define ``_model`` which is the type of the model to instantiate.
3. Define ``_model_builder`` to point to the
   :doc:`model builder </usage/model_builders>` 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
:py:func:`@schema() <pyfactory.schema.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
:py:class:`Factory <pyfactory.factory.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 :py:class:`factory API <pyfactory.factory.Factory>` 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:

* :py:meth:`attributes <pyfactory.factory.Factory.attributes>` -
  This will return the raw dictionary of the schema.
  This doesn't invoke your model builder at all.
* :py:meth:`build <pyfactory.factory.Factory.build>` -
  This will return an instance of your model, but will not
  persist it to any backing store.
* :py:meth:`create <pyfactory.factory.Factory.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.
