Extending tn-venv
The extension points are ordinary Python classes. This page shows the two most common ones — a new activator and a new seeder — and where to register them.
If you only need to observe the pipeline (log timestamps, dump metadata, write extra files into the new environment, install extra packages, …) you almost certainly want the Plugins instead — it is the stable, public hook surface and needs no patching of the upstream code.
Writing an activator
An activator is a class with a name and a templates mapping of
file name → template text. Placeholders use the __VENV_*__ convention
and are substituted with shell-appropriate quoting by the base class.
# tn_venv/create/activators/elvish.py
from .base import Activator
ELVISH_ACTIVATE = """\
# generated by tn-venv
set-env VIRTUAL_ENV __VENV_DIR_SH__
set-env PATH "__VENV_BIN_PATH_SH__:"$E:PATH
"""
class ElvishActivator(Activator):
name = "elvish"
templates = {"activate.elv": ELVISH_ACTIVATE}
executable_names = ("activate.elv",) # chmod +x on POSIX
Available placeholders:
Placeholder |
Content |
Quoting |
|---|---|---|
|
environment directory |
raw |
|
folder name |
raw |
|
prompt text |
raw |
|
|
raw |
|
same, forward slashes |
raw |
|
environment python path |
raw |
|
shell-quoted forms |
POSIX sh |
|
PowerShell-quoted forms |
PowerShell |
|
fish-quoted forms |
fish |
If a shell needs a placeholder in a new quoting style, add a _xxx_quote
helper to Activator and expose a __VENV_*__XXX__ key — do not quote in
the subclass.
Register the class in tn_venv/create/activators/__init__.py by adding it
to the _ALL tuple; --activators elvish and the docs table pick it up
from there.
Writing a seeder
A seeder receives the finished CreatorContext and a Reporter, and
returns a SeedResult:
from tn_venv.seed.seeder import Seeder, SeedResult
from tn_venv.util.process import clean_pip_env, run_cmd
class UvSeeder(Seeder):
name = "uv"
def __init__(self, packages=None, **_ignored):
self.packages = packages or []
def seed(self, ctx, report):
if not self.packages:
return SeedResult(skipped=True)
run_cmd(
["uv", "pip", "install", "--python", str(ctx.env_exe), *self.packages],
env=clean_pip_env(),
report=report,
)
return SeedResult(packages=list(self.packages))
Register it in tn_venv/seed/seeder.py::make_seeder. Seeders must:
invoke the environment interpreter as
str(ctx.env_exe), never"python";use
clean_pip_env()(or an equivalent scrub) so the host’sPYTHONHOME/PYTHONPATHcannot leak in;raise
SeedErroron failure, with the failing command in the message.
Adding a new option
Options are declared once in tn_venv/config/spec.py:
OptionSpec(
"my_option", ("--my-option",),
kind="bool", default=False,
help="what it does",
)
That single declaration gives you the CLI flag (with --no-my-option for
booleans), the TN_VENV_MY_OPTION environment variable, the my-option
config-file key, and --dry-run output. The value arrives on
Options.my_option; consume it in session.run_session().
Testing your extension
Every built-in stage has a matching tests/test_*.py; follow
Testing. Activator tests assert on generated content and refuse
unsubstituted placeholders; seeder tests mock run_cmd so the suite never
touches the network.