Creating custom fields

Custom fields can be created by extending from one of the field classes in headfake.field. Headfake uses the attrs package to provide a simpler way of initialising and handling class properties, although this does make it a little more difficult to sub-class field classes if the generated data is similar to that generated by other field types.

To deal with this Headfake provides a DerivedField class which acts as a decorator around another field (created through the _internal_field method).

In some cases, what you want to achieve may be better achieved by using a Transformer class instead, for example processing the generated value in some way.

An example derived custom field

Below is a simple derived custom checkbox field which uses an internal OptionValueField to generate a 0 or 1 value. The user only provides a single 'yes_probability' to use the field.

An example checkbox field could look like this.

@attr.s(kw_only=True)
class YesNoUnsureField(DerivedField):
    yes_probability = attr.ib() #probability that Y is generated
    unsure_probability = attr.ib()

    def _internal_field(self):
        ynu_probs = {
            2:self.unsure_probability,
            1:self.yes_probability,
            0:1-self.yes_probability-self.unsure_probability,

        }

        return OptionValueField(probabilities=ynu_probs)

..

ynu_field = YesNoUnsureField(yes_probability=0.4, unsure_probablity=0.05)

An example non-derived field

When generated data is completely different from any existing functionality then you should extend headfake.field.Field and create appropriate attributes. Then over-ride the _next_value function to create the value.

For example, this field will continuously rotate through a list of supplied characters with a different one one each row.

@attr.s(kw_only=True)
class RotatingCharacterField(Field):
    characters = attr.ib()
    _curr_char_pos = attr.ib(default=0)

    def _next_value(self, row):
        char = self.characters[self._curr_char_pos]
        self._curr_char_pos+=1
        if self._curr_char_pos>len(self.characters):
            self.curr_char_pos = 0

        return char

Using custom fields in YAML templates

This is as simple as entering the classname in the 'class' property in the YAML file along with the additional parameters. For example to use the RotatingCharacterField:

my_character:
    class: mypackage.RotatingCharacterField
    characters: ABCDEFGH

Providing your package is within the Python library path, it should be found and used to generate the field values.

Adding tests for a custom field

It is recommended that a test file is setup for new custom fields, particularly if the logic is complex. To aid this, a field class can be created standalone the next_value method can be tested.

Below is an example using pytest to test the logic for the RotatingCharacterField:

import pytest

def test_characters_are_rotated():
    rc_field = RotatingCharacterField(characters=["ABC"])
    assert rc_field.next_value(row={}) == "A"
    assert rc_field.next_value(row={}) == "B"
    assert rc_field.next_value(row={}) == "C"
    assert rc_field.next_value(row={}) == "A"

It can be difficult to handle randomly generated fields so you can either replace functions used internally with ones which you can control or you can set the random seed to a known value. For example in the CheckboxField.

def test_CheckboxField_returns_expected_value(monkeypatch):
    HeadFake.set_seed(10)

    assert ov.next_value(row) == 1
    assert ov.next_value(row) == 1
    assert ov.next_value(row) == 0
    assert ov.next_value(row) == 1