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.

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:

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 build_file_prelude_globs in the [GLOBAL] scope:

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

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

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

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

A macro can create multiple targets:

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:

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)
custom_python_sources(
    has_type_hints=False,
)

Did this page help you?