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