Skip to main content
Version: 2.0 (deprecated)

Creating new fields

How to create a Field, including the available templates.


Before creating a new target type, the first step is to create all of the target type's fields.

Defining a Field

To define a new field:

  1. Subclass one of the below field templates, like IntField or BoolField; or, subclass a pre-existing field, like Sources.
  2. Define the class property alias. This is the symbol that people use in BUILD files.

For example:

from pants.engine.target import IntField

class TimeoutField(IntField):
"""How long to run until timing out."""

alias = "timeout"

Changing the default

The default is used whenever a user does not explicitly specify the field in a BUILD file.

Set the class property default = $val, where $val is the value you want.

If you don't override this property, default will be set to None, which signals that the value was undefined.

Marking a field as required

Set the class property required = True to enforce that users must explicitly define the field.

If you set required = True, the default value will be ignored.

FYI: Pants uses the Docstring to generate docs

When you run ./pants help my_target, Pants will use your field definition to generate the output. It will look at the class's docstring, the field template used, and whether required = True.

Reminder: subclass pre-existing fields to modify their behavior

If you want to change how a pre-existing field behaves, you should subclass the original field. For example, if you want to change a default value, subclass the original field.

See Concepts for how subclassing plays a key role in the Target API.

Adding custom validation

The field templates will validate that users are using the correct types, like ints or strings. But you may want to add additional validation, such as banning certain values.

To do this, override the classmethod compute_value:

from pants.engine.target import IntField, InvalidFieldException

class UploadTimeout(IntField):
...

@classmethod
def compute_value(
cls, raw_value: Optional[int], *, address: Address
) -> int:
value_or_default = super().compute_value(raw_value, address=address)
if value_or_default < 10 or value_or_default > 300:
raise InvalidFieldException(
f"The {repr(cls.alias)} field in target {address} must "
f"be between 10 and 300, but was {value_or_default}."
)
return value_or_default

Be careful to use the same type hint for the parameter raw_value as used in the template. This is used to generate the documentation in ./pants help my_target.

Available templates

All templates are defined in pants.engine.target.

BoolField

Use this when the option is a boolean toggle.

If you do not set required = True or give a default, the default will be None. This can be useful to represent three states: unspecified, False, and True.

IntField

Use this when you expect an integer. This will reject floats.

FloatField

Use this when you expect a float. This will reject integers.

StringField

Use this when you expect a single string.

StringField can be like an enum

You can set the class property valid_choices to limit what strings are acceptable. This class property can either be a tuple of strings or an enum.Enum.

For example:

class LeafyGreensField(StringField):
alias = "leafy_greens"
valid_choices = ("kale", "spinach", "chard")

or:

class LeafyGreens(Enum):
KALE = "kale"
SPINACH = "spinach"
CHARD = "chard"

class LeafyGreensField(StringField):
alias = "leafy_greens"
valid_choices = LeafyGreens

ScalarField (advanced)

Use this when you expect a single value, but it is a non-primitive type. For example, use this with any objects you defined.

You must set the class properties expected_type and expected_type_description. You should also change the type signature of the classmethod compute_value so that Pants can show the correct types when running ./pants help $target_type.

For example:

class DemoScalarField(ScalarField):
alias = "demo_scalar"
expected_type = MyObject
expected_type_description = "a `my_object()` instance"

@classmethod
def compute_value(
raw_value: Optional[MyObject], *, address: Address
) -> Optional[MyObject]:
return super().compute_value(raw_value, address=address)

StringSequenceField

Use this when you expect 0-n strings.

The user may use a tuple, set, or list in their BUILD file; Pants will convert the value to an immutable tuple.

SequenceField

Use this when you expect a homogenous sequence of values other than strings, such as a sequence of integers.

The user may use a tuple, set, or list in their BUILD file; Pants will convert the value to an immutable tuple.

You must set the class properties expected_element_type and expected_type_description. You should also change the type signature of the classmethod compute_value so that Pants can show the correct types when running ./pants help $target_type.

class ExampleIntSequence(SequenceField):
alias = "int_sequence"
expected_element_type = int
expected_type_description = "a sequence of integers"

@classmethod
def compute_value(
raw_value: Optional[Iterable[int]], *, address: Address
) -> Optional[Tuple[int, ...]]:
return super().compute_value(raw_value, address=address)

DictStringToStringField

Use this when you expect a dictionary of string keys with strings values, such as {"k": "v"}.

The user may use a normal Python dictionary in their BUILD file. Pants will convert this into an instance of pants.util.frozendict.FrozenDict, which is a light-weight wrapper around the native dict type that simply removes all mechanisms to mutate the dictionary.

DictStringToStringSequenceField

Use this when you expect a dictionary of string keys with a sequence of strings values, such as {"k": ["v1", "v2"]}.

The user may use a normal Python dictionary in their BUILD file, and they may use a tuple, set, or list for the dictionary values. Pants will convert this into an instance of pants.util.frozendict.FrozenDict, which is a light-weight wrapper around the native dict type that simply removes all mechanisms to mutate the dictionary. Pants will also convert the values into immutable tuples, resulting in a type hint of FrozenDict[str, Tuple[str, ...]].

PrimitiveField - the fallback class

If none of these templates work for you, you can subclass PrimitiveField, which is the superclass of all of these templates.

You must give a type hint for value, define the classmethod compute_value, and either set required = True or define the class property default.

For example, we could define a StringField explicitly like this:

from typing import Optional

from pants.engine.addresses import Address
from pants.engine.target import InvalidFieldTypeException, PrimitiveField


class VersionField(PrimitiveField):
alias = "version"
value: Optional[str]
default = None

@classmethod
def compute_value(
cls, raw_value: Optional[str], *, address: Address
) -> Optional[str]:
value_or_default = super().compute_value(raw_value, address=address)
if value_or_default is not None and not isinstance(value, str):
# A helper exception message to generate nice error messages
# automatically. You can use another exception if you prefer.
raise InvalidFieldTypeException(
address, cls.alias, raw_value, expected_type="a string",
)
return value_or_default
Asking for help

Have a tricky field you're trying to write? We would love to help! See Getting Help.

Examples

plugins/target_types.py
from typing import Optional

from pants.engine.target import (
BoolField,
IntField,
InvalidFieldException,
Sources,
StringField
)


class FortranVersion(StringField):
"""Which version of Fortran should this use?"""

alias = "fortran_version"
required = True
valid_choices = ("f95", "f98")


class CompressToggle(BoolField):
"""Whether to compress the generated file."""

alias = "compress"
default = False


class UploadTimeout(IntField):
"""How long to upload (in seconds) before timing out.

This must be between 10 and 300 seconds.
"""

alias = "upload_timeout"
default = 100

@classmethod
def compute_value(
cls, raw_value: Optional[int], *, address: Address
) -> int:
value_or_default = super().compute_value(raw_value, address=address)
if value_or_default < 10 or value_or_default > 300:
raise InvalidFieldException(
f"The {repr(cls.alias)} field in target {address} must "
f"be between 10 and 300, but was {value_or_default}."
)
return value_or_default


# Example of subclassing a pre-existing field.
# We don't need to define `alias = sources` because the
# parent class does this already.
class FortranSources(Sources):
default = ("*.f95",)