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.
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 file
/ files
or resource
/ 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 FileSourceField, ResourceSourceField
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.rules import Get, rule
from pants.engine.target import SourcesField
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(SourcesField) for tgt in repl.targets),
for_sources_types=(BashSourceField, FileSourceField, ResourceSourceField),
),
)
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.