Add a REPL

How to add a new implementation to the repl goal.

The repl goal opens up an interactive Read-Eval-Print Loop that runs in the foreground.

Typically, the REPL is loaded with the transitive closure of the files and targets that the user provided, so that users may import their code and resources in the REPL.

πŸ“˜

Example repository

This guide walks through adding a simple repl implementation for Bash that runs /bin/bash and ensures all relevant files are included in the temporary directory. See here for the final implementation.

1. Install your REPL

There are several ways for Pants to install your REPL. See Installing tools.

In this example, we simply find the program bash on the user's machine, but often you will want to install a tool like Ammonite or iPython instead.

You may want to also add options for your REPL implementation, such as allowing users to change the version of the tool. See Options and subsystems.

2. Set up a subclass of ReplImplementation

Subclass ReplImplementation and define the class property name: str with the name of your REPL, e.g. "bash" or "ipython". Users can then set the option --repl-shell to this option to choose your REPL implementation.

from pants.core.goals.repl import ReplImplementation

class BashRepl(ReplImplementation):
    name = "bash"

Then, register your new ReplImplementation with a UnionRule so that Pants knows your REPL implementation exists:

from pants.engine.rules import collect_rules
from pants.engine.unions import UnionRule

...

def rules():
    return [
        *collect_rules(),
        UnionRule(ReplImplementation, BashRepl),
    ]

3. Create a rule for your REPL logic

Your rule should take as a parameter the ReplImplementation from Step 2, which has a field targets: Targets containing the transitive closure of the targets/files provided by the user. You can then filter these targets to only include the Sources you care about.

Your rule should return ReplRequest, which has the fields digest: Digest, args: Iterable[str], and extra_env: Optional[Mapping[str, str]].

The ReplRequest will get converted into an InteractiveProcess that will run in the foreground.

The process will run in a temporary directory in the build root, which means that the script/program can access files that would normally need to be declared by adding a files or resources target to the dependencies field.

The process's environment will not be hermetic, meaning that it will inherit the environment used by the ./pants process. Any values you set in extra_env will add or update the specified environment variables.

from dataclasses import dataclass

from pants.core.goals.repl import ReplRequest
from pants.core.target_types import FilesSources, ResourcesSources
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.rules import Get, rule
from pants.engine.target import Sources
from pants.util.logging import LogLevel

...

@rule(level=LogLevel.DEBUG)
async def create_bash_repl_request(repl: BashRepl) -> ReplRequest:
    # First, we find the `bash` program.
    bash_program_paths =  await Get(
        BinaryPaths, BinaryPathRequest(binary_name="bash", search_path=("/bin", "/usr/bin")),
    )
    if not bash_program_paths.first_path:
        raise EnvironmentError("Could not find the `bash` program on /bin or /usr/bin.")
    bash_program = bash_program_paths.first_path

    # `repl.targets` already includes the transitive closure of the input targets. We filter out
    # irrelevant soures.
    sources = await Get(
        SourceFiles,
        SourceFilesRequest(
            (tgt.get(Sources) for tgt in repl.targets),
            for_sources_types=(BashSources, FilesSources, ResourcesSources),
        ),
    )
    return ReplRequest(
        digest=sources.snapshot.digest, args=(bash_program.exe,)
    )

If you use any relative paths in args or extra_env, you should call repl.in_chroot("./example_relative_path") on the values. This ensures that you run on the correct file in the temporary directory created by Pants.

Finally, update your plugin's register.py to activate this file's rules.

from bash import repl


def rules():
    return [*repl.rules()]

Now, when you run ./pants repl --shell=bash ::, your new REPL should be used.


Did this page help you?