Source code for pyfactory.field

"""
Contains the special field types for PyFactory.
"""

from .error import InvalidModelBuilderError

[docs]class Field(object): """ If you wish to implement a custom field which has special behavior, you must inherit and implement the methods in this class. Fields are how things such as :py:func:`association` and :py:func:`sequence` are implemented. """
[docs] def resolve(self, scope): """ This method is called by PyFactory to resolve the value of a special field. The ``scope`` argument will be one of ``attributes``, ``build``, or ``create`` depending on what method was called on the factory. The return value of this method is what is put into the actual attributes dict for the model. """ raise NotImplementedError
[docs]class AssociationField(Field): """ This field represents an association with another factory model. """ def __init__(self, factory, schema, attr=None): """ This marks the field value as the result of creating another model from another factory. If ``attr`` is specified, then that attribute value will be the value of the association. This requires that the model builder understand the ``getattr`` class method, otherwise an exception will be raised. :Parameters: - `factory`: An instance of a :class:`Factory` to get the model from. - `schema`: The name of the schema to load. - `attr` (optional): The name of the attribute to read from the resulting model to place as the value of this field. If not given, the entire model becomes the value of the field. """ self.factory = factory self.schema = schema self.attr = attr self.loaded_schema = None def _load_schema(self, scope): """ This loads the schema for the given scope. This method exists so that the schema is only loaded once. """ if self.loaded_schema is None: self.loaded_schema = getattr(self.factory, scope)(self.schema) return self.loaded_schema def _read_attribute(self, scope, attr): """ This reads the attribute out of the loaded schema based on the scope. This function is built as a utility to "do the right thing." For example, when loading attributes, this does a simple dictionary lookup. But when building or creating, this invokes the model builder to read the attribute, since it knows best. """ loaded_schema = self._load_schema(scope) if scope == "attributes": # Attributes return dictionaries, so we just use the # dictionary syntax to get it... return loaded_schema[attr] else: # Otherwise we have a model, and we have to use the # getattr method on the model builder. builder = self.factory._model_builder if not hasattr(builder, "getattr") or not callable(builder.getattr): raise InvalidModelBuilderError("Model builder must implement `getattr` for associations") return self.factory._model_builder.getattr(loaded_schema, attr) def resolve(self, scope): result = self._load_schema(scope) if self.attr is not None: result = self._read_attribute(scope, self.attr) return result
[docs] def attribute(self, attr): """ :Parameters: - `attr`: The name of the attribute to resolve to. This will return a new :py:class:`Field` instance which resolves to the value of the given ``attr`` of this association. This method is useful to re-use a single association multiple times for different values. For example, say you have a schema which uses the ``id`` and the ``name`` field of a user. You could then define the schema like so:: @schema() def example(self): user = association(UserFactory(), "basic") return { "remote_id": user.attribute("id"), "remote_name": user.attribute("name") } The benefit of the above, instead of using two separate ``associations``, is that the association in this case will resolve to the exact same model, whereas two ``associations`` will always resolve to two different models. """ return _AssociationFieldAttribute(self, attr)
[docs] def callback(self, callback): """ :Parameters: - `callback`: Callback to be called, with the association as a parameter. This will return a new :py:class:`Field` instance which when resolved will call the given callback, passing the association as a parameter. To read an attribute from the association, use the dictionary syntax of ``association[key]``. This will do the right thing depending on whether you're resolving attributes or a model build. As an example, let's say you want to create a schema which depends on the concatenated first and last name of a user. Here is an example:: @schema() def example(self): def get_name(association): return "%s %s" % \\ (association["first_name"], association["last_name"]) user = association(UserFactory(), "basic") return { "name": user.callback(get_name) } """ return _AssociationFieldCallback(self, callback)
class _AssociationFieldAttribute(Field): """ Internal class used to grab an attribute from another :py:class:`AssociationField`. """ def __init__(self, association, attr): self.association = association self.attr = attr def resolve(self, scope): return self.association._read_attribute(scope, self.attr) class _AssociationFieldCallback(Field): """ Internal class used to call a callback to determine the resolution value. """ def __init__(self, association, callback): self.association = association self.callback = callback def resolve(self, scope): self._scope = scope self.association._load_schema(scope) return self.callback(self) def __getitem__(self, key): # We pass "None" as the scope since by the time this is called, # it will be from the callback, which is after we load the schema, # so we can be sure it is loaded. return self.association._read_attribute(self._scope, key)
[docs]class SequenceField(Field): """ This field allows for easily creating values which have a single sequential piece in it. """ COUNTS = {} def __init__(self, string, interpolation_variable="n", unique_key="_default"): """ This causes the value of a field to be interpolated with a sequential value. The ``interpolation_variable`` will be the variable name used for interpolation in the string. :Parameters: - `string`: The string to perform the sequence interpolation on. - `interpolation_variable` (optional): The name of the variable to use for interpolation in the ``string``. - `unique_key` (optional): The unique key to base the sequence creation on. By default, every time you call sequence, no matter what model factory or schema you're in, the sequence will increase. By specifying a unique ``unique_key``, it isolates the increment to that key. """ self.string = string self.interpolation_variable = interpolation_variable self.unique_key = unique_key def resolve(self, scope): SequenceField.COUNTS.setdefault(self.unique_key, 0) SequenceField.COUNTS[self.unique_key] += 1 return self.string % \ { self.interpolation_variable: SequenceField.COUNTS[self.unique_key] }
[docs]def association(*args, **kwargs): """ Shortcut method for enabling an association field. See documentation of :py:class:`AssociationField`. """ return AssociationField(*args, **kwargs)
[docs]def sequence(*args, **kwargs): """ Shortcut method for enabling a sequence field. See documentation of :py:class:`SequenceField`. """ return SequenceField(*args, **kwargs)