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.

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.