Skip to content

feat: split environments from tasks (env:task) - #1135

Draft
henryiii wants to merge 3 commits into
wntrblm:mainfrom
henryiii:henryiii/refactor/envrunner
Draft

feat: split environments from tasks (env:task)#1135
henryiii wants to merge 3 commits into
wntrblm:mainfrom
henryiii:henryiii/refactor/envrunner

Conversation

@henryiii

@henryiii henryiii commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

WIP! This is the whole idea, so you can see the end goal, it's designed to be mergable in 3-4 stages.

🤖 AI text below 🤖

Splits the fused session concept into environments (a virtualenv + how to provision it) and tasks (what to run), so several tasks can share one venv, environments can live at a chosen path like .venv, and lock files get first-class, extensible support.

tooling = nox.env("tooling", python="3.12", dependencies=["prek", "mypy"])

@tooling.task
def lint(session): ...          # session id: tooling:lint

dev = nox.env.uv("dev", location=".venv")   # uv sync --locked into .venv
ci = nox.env.pylock("ci")                   # PEP 751 pylock via uv pip sync

nox.alias("check", "tooling:lint", "tooling:typecheck")
  • A session is now an env:task pair; @nox.session is sugar for a same-name pair, so existing noxfiles are unchanged (naming, envdirs, and expansion are byte-identical — verified against this repo's own noxfile).
  • One identifier grammar across -s, requires=, and session.notify(): full ids, aliases (recursive), environment/instance selectors (default tasks), and unambiguous bare task names; ambiguity errors list the candidates.
  • Declarative environments record their inputs in a stamp file and are reused by default: unchanged inputs skip installs entirely, changed lock files re-sync in place, and changed plain dependency lists recreate the env so removed packages actually disappear. -r/-R/--reuse-venv semantics are untouched for classic sessions.
  • location= safety: nox refuses to delete a non-empty directory that isn't a virtualenv, never writes .gitignore/CACHEDIR.TAG outside --envdir, and nox.env.uv refuses to sync off the uv backend so --force-venv-backend cannot clobber a project's own .venv.
  • Third-party lock formats subclass nox.Environment and override stamp_data()/sync() — no plugin registry needed.

The four commits are ordered for review: runner split (no behavior change) → env/task/alias model → provisioning/lock files/locations → docs. An adversarial review pass (backend overrides, CLI edge cases, staleness lies, name collisions) was run and its findings are folded into the relevant commits with regression tests; end-to-end flows (uv.lock → .venv, stamp skip/resync, ownership refusal) were exercised live in addition to the unit suite.

Design decisions that deserve reviewer eyes: declarative-object API with @env.setup as the dynamic escape hatch; nox -s <env> running default tasks; reuse-by-default for declarative envs only; provisioning as inline memoized calls on a shared EnvRunner rather than hidden graph nodes.

@henryiii
henryiii force-pushed the henryiii/refactor/envrunner branch 3 times, most recently from b4f5f0e to 1b2bb5b Compare July 9, 2026 13:44
@henryiii
henryiii force-pushed the henryiii/refactor/envrunner branch 2 times, most recently from d6e61d3 to bb10ade Compare July 31, 2026 14:16
henryiii added 3 commits July 31, 2026 22:14
First step of the environment/task split: a new EnvRunner owns the
virtualenv lifecycle (envdir, reuse decision, creation) with an
idempotent ensure(), paired 1:1 with SessionRunner for now.
SessionRunner keeps _create_venv/reuse_existing_venv/venv/envdir as
delegating members, so behavior and the test-facing surface are
unchanged.

Assisted-by: ClaudeCode:claude-fable-5
Environments (venv + provisioning config) are now first-class and
separate from tasks:

- nox.env(name, python=..., venv_backend=..., ...) declares an
  Environment; @env.task attaches tasks that share its single venv
  (one EnvRunner per concrete environment instance, configured from
  the environment rather than whichever task runs first).
- Sessions are env:task pairs. @nox.session is now sugar for a
  same-name pair and keeps its exact naming, envdir, and expansion
  behavior; historically legal names containing ':'/'(' keep working
  with a FutureWarning.
- nox.alias(name, *targets) registers a global alias; aliases expand
  recursively at selection time and warn when they shadow a task name.
- Environments and aliases are globally unique; task names only
  within their environment.
- One identifier grammar everywhere: -s, requires=, and
  session.notify() all accept env:task ids, aliases, environment or
  instance selectors (which expand to default tasks; an environment
  with no default tasks errors instead of silently selecting
  nothing), and unambiguous bare task names. Instance selectors like
  tooling-3.12 also work for single-python environments.
- Python lists expand at the environment level (tests-3.11:run);
  @nox.parametrize stays task-level and shares the environment venv.
- Manifest construction errors surface as clean exit-3 messages.
- --json listing gains env and task fields.

The registry now stores envs/tasks/aliases (storage lives in
nox/environments.py so imports stay one-directional);
registry.get() keeps the legacy mapping view.

Assisted-by: ClaudeCode:claude-fable-5
Environments can now declare how they are provisioned:

- dependencies=[...] installs with pip/uv; an optional @env.setup hook
  covers dynamic needs (making the environment unstampable unless
  setup_stamp is set). Declaring dependencies on a venv-less
  environment fails with a clear error.
- nox.env.pylock installs a PEP 751 pylock file via 'uv pip sync';
  nox.env.uv syncs a uv project with 'uv sync --locked'. Both guard
  the backend at sync time, so --force-venv-backend/--no-venv cannot
  make 'uv sync' fall back to (and clobber) the project's own .venv.
  Third parties subclass nox.Environment and override
  stamp_data()/sync().
- A stamp file (.nox-env.json) inside the environment records the
  provisioning inputs; when it matches on reuse, syncing is skipped
  entirely. When it does not match, exact syncs (lock files) re-sync
  in place, while additive ones (plain dependency lists) recreate the
  environment so removed dependencies actually disappear
  (Environment.sync_is_exact). Declarative environments reuse by
  default; --reuse-venv=never still recreates, -R/--no-install still
  skip installs. Classic sessions are unchanged.
- location=".venv" places the environment at a user path (relative to
  the noxfile). Location collisions (including a missing
  {name}/{python} placeholder on a multi-python environment) are
  checked over the *selected* sessions at filter time, so a located
  environment cannot break unrelated invocations under
  --force-python. nox refuses to touch a non-empty location that is
  not a virtual environment, and does not write
  .gitignore/CACHEDIR.TAG outside --envdir.
- session.install() into a shared multi-task environment warns once
  (unless the install is skipped by -R/--no-install), pointing at
  declarative dependencies.

Assisted-by: ClaudeCode:claude-fable-5
@henryiii
henryiii force-pushed the henryiii/refactor/envrunner branch from bb10ade to 3f4f69d Compare August 1, 2026 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant