Skip to main content
Version: 2.23

Macros

Reducing boilerplate in BUILD files.


When to use a macro

Macros are useful to reduce boilerplate in BUILD files. For example, if you keep using the same value for a field, you can use a macro.

However, also consider that introducing new symbols to BUILD files adds some indirection to your codebase, such as making it harder to follow along with the Pants docs. As with any tool, macros should be used judiciously.

Often, you can instead use the parametrize mechanism:

BUILD
shell_tests(
name="tests",
shell=parametrize("bash", "zsh"),
)

If you instead want to add support for a new language, or do something more complex than a macro allows, create a new target type.

If you are already using a target type, but need to store additional metadata for your plugin, add a new field to the target type.

How to add a macro

Macros are defined in Python files that act like a normal BUILD file. They have access to all the symbols you normally have registered in a BUILD file, such as all of your target types.

Macros cannot import other modules, just like BUILD files cannot have import statements.

To define a new macro, add a function with def and the name of the new symbol. Usually, the last line of the macro will create a new target, like this:

pants-plugins/macros.py
def python2_sources(**kwargs):
kwargs["interpreter_constraints"] = ["==2.7.*"]
python_sources(**kwargs)

def python3_sources(**kwargs):
kwargs["interpreter_constraints"] = [">=3.5"]
python_sources(**kwargs)

Then, add this file to the option [GLOBAL].build_file_prelude_globs:

pants.toml
[GLOBAL]
build_file_prelude_globs = ["pants-plugins/macros.py"]

Now, in BUILD files, you can use the new macros:

project/BUILD
python2_sources(
name="app_py2",
sources=["app_py2.py"],
)

python3_sources(
name="app_py3",
sources=["app_py3.py"],
)

A macro can create multiple targets—although often it's better to use parametrize:

pants-plugins/macros.py
def python23_tests(name, **kwargs):
kwargs.pop("interpreter_constraints", None)

python_tests(
name=f"{name}_py2",
interpreter_constraints=["==2.7.*"],
**kwargs,
)

python_tests(
name=f"{name}_py3",
interpreter_constraints=[">=3.5"],
**kwargs,
)

A macro can perform validation:

pants-plugins/macros.py
def custom_python_sources(**kwargs):
if "2.7" in kwargs.get("interpreter_constraints", ""):
raise ValueError("Python 2.7 is banned!")
python_sources(**kwargs)

A macro can take new parameters to generate the target dynamically. For example:

def custom_python_sources(has_type_hints: bool = True, **kwargs):
if has_type_hints:
kwargs["tags"] = kwargs.get("tags", []) + ["type_checked"]
python_sources(**kwargs)

Documenting your macros

Using doc-strings to document your macros, Pants will pick up and present these for the online help text. Also global constants can be documented using Doc wrapped in Annotated.

pants-plugins/macros.py
OUR_GLOBAL_CONSTANT: Annotated[
int,
Doc(
"""This is our magic number.

It is useful when you need the answer to the meaning of life, the universe and everythin.
"""
)
] = 42


def custom_python_sources(has_type_hints: bool = True, **kwargs):
"""Custom target for Python sources.

This target adds the `type_checked` tag for targets for which `has_type_hints` is true.
"""
# ...

For CLI help, this information is presented when calling pants with:

pants OUR_GLOBAL_CONSTANT --help
pants custom_python_sources --help

To list all available symbols, along with the first sentence of the docs for each:

pants symbols --help

# To also include targets (to list only targets, there is "pants targets --help"):
pants symbols --help-advanced