# 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. ```python # 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`: ```python 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`: ```python 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 {doc}`testing`. Activator tests assert on generated content and refuse unsubstituted placeholders; seeder tests mock `run_cmd` so the suite never touches the network.