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:
- Subclass one of the below field templates, like
IntField
orBoolField
; or, subclass a pre-existing field, likeSources
. - 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
.
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 enumYou 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
Have a tricky field you're trying to write? We would love to help! See Getting Help.
Examples
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",)