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

__VENV_DIR__

environment directory

raw

__VENV_NAME__

folder name

raw

__VENV_PROMPT__

prompt text

raw

__VENV_BIN_NAME__

Scripts or bin

raw

__VENV_BIN_NAME_SLASH__

same, forward slashes

raw

__VENV_PYTHON__

environment python path

raw

__VENV_DIR_SH__, __VENV_BIN_PATH_SH__, __VENV_PROMPT_SH__

shell-quoted forms

POSIX sh

__VENV_DIR_PS__, __VENV_PROMPT_PS__

PowerShell-quoted forms

PowerShell

__VENV_DIR_FISH__, __VENV_PROMPT_FISH__

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’s PYTHONHOME/PYTHONPATH cannot leak in;

  • raise SeedError on 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.