Skip to content

ADR-0018: PEP 735 dependency groups are the registry of FineCode environments

  • Status: proposed
  • Date: 2026-04-11
  • Deciders: @Aksem
  • Tags: configuration, environments, packaging

Context

FineCode runs handlers in purpose-specific virtual environments. A FineCode project declares environments in two places in pyproject.toml:

  • [dependency-groups] — standard PEP 735 groups that any PEP 735–aware tool (uv, pip 25.1+) can read. Each group is a list of PEP 508 requirement strings, optionally including other groups via { include-group = "..." }.
  • [tool.finecode.env.<name>.dependencies] — a FineCode-native table where each entry is a dependency descriptor that may carry fields PEP 508 cannot express, such as editable path installs ({ path = "./pkg", editable = true }).

The relationship between the two has so far been implicit. The create_envs and install_envs discovery handlers list environments by iterating the keys of [dependency-groups], and FineCode's install logic treats the [tool.finecode.env.<name>.dependencies] table as a supplement that rewrites matching entries in the corresponding PEP 735 group (and appends entries that had no prior string form). With the rule unwritten, three problems surfaced:

  • A FineCode environment could be declared only under [tool.finecode.env.<name>.dependencies], with no [dependency-groups] entry at all. Such an environment was invisible to env discovery and never got a venv created, even though FineCode would not warn about it.
  • Cross-group composition via { include-group = "<name>" } is valid PEP 735 only when the referenced group exists in [dependency-groups]. Tools like uv reject a group reference that does not. This blocked a dev group from pulling in a runtime group that lived only under [tool.finecode.env.*].
  • A temptation arose to paper over the validation error by adding an empty runtime = [] stub. That is valid PEP 735, but it turns the config file into something a non-FineCode reader has to be told about, and it hides the real question — what does "a FineCode environment" mean at the configuration layer?

These problems share one root cause: there was no stated rule for which table owns the list of FineCode environments. This ADR writes that rule down.

  • ADR-0003 — each Extension Runner runs in its own process tied to one environment. That ADR establishes why environments exist; this ADR establishes how they are declared.
  • ADR-0006 — defines the contract between action declarations and their runtime environment. This ADR is the configuration-surface counterpart: it fixes where the environment identifier a handler references must be declared.
  • Reviewed the remaining ADRs (0001–0002, 0004–0005, 0007–0017). None overlap with the configuration surface for environments.

Decision

[dependency-groups] is the single registry of FineCode environments. [tool.finecode.env.<name>.dependencies] is a supplement that may refine entries in the corresponding group but never introduces new environments.

The rule has four parts:

  1. Every FineCode environment corresponds to a [dependency-groups] entry of the same name. An environment that appears only under [tool.finecode.env.*] is a configuration error. Env discovery, virtualenv creation, and dependency installation all key off the [dependency-groups] table; anything declared outside it is unreachable.

  2. [tool.finecode.env.<name>.dependencies] is install-time supplement only. Its sole purpose is to express installation details that PEP 508 requirement strings cannot carry — at time of writing, editable path installs. FineCode's install logic merges each entry into the matching PEP 735 group before handing dependencies to the install handler. The supplement never defines an environment, never defines a dependency that is not already listed (or referenced through the project) by the PEP 735 group, and is ignored by non-FineCode tools by design.

  3. Environments that need the project's runtime dependencies reference the project itself by name. A group entry such as dev = ["finecode", "pytest==7.4.*"] pulls [project.dependencies] transitively through the project package, keeping runtime deps declared in exactly one place. Editable path overrides for sub-packages of the project live in [tool.finecode.env.<name>.dependencies] as in rule 2. Re-listing runtime deps inside a dependency group — or inside the FineCode supplement table — is prohibited.

  4. Environment composition uses standard PEP 735 include-group. FineCode does not introduce a parallel composition primitive. Because every environment is a real [dependency-groups] entry by rule 1, include-group always resolves against a present group and uv-style validators accept the file unchanged.

Consequences

  • A pyproject.toml remains readable by non-FineCode tools. Every environment is a conventional PEP 735 group with a conventional shape. uv sync --group=dev works for an outside contributor who does not run FineCode; the supplement table is simply ignored by tools that do not understand it, without altering group semantics.

  • No duplication of [project.dependencies]. Rule 3 keeps runtime dependencies in one place and lets group entries that need them pull them in through the project package. Previously the temptation was to re-list runtime deps under an environment-specific group; this is now explicitly rejected.

  • Env discovery becomes a one-line invariant. The set of FineCode environments for a project equals the set of keys in [dependency-groups]. Discovery handlers do not need to consult [tool.finecode.env.*] to decide what to create.

  • Config errors are catchable. An environment defined only under [tool.finecode.env.*] is now a rule violation, so FineCode can surface it as a configuration error instead of silently producing no venv. Implementations of the discovery handlers should warn or fail when they encounter such an entry.

  • Empty placeholder groups are unnecessary. The two historical motives for an empty group — reserving an environment name and satisfying include-group validation — are both removed. An environment name is reserved by declaring a real group with real contents (rule 3); an include-group reference resolves because the referenced group always exists (rules 1 and 4).

  • The supplement table has a narrow, stable purpose. As long as PEP 508 requirement strings cannot express a capability FineCode needs (editable paths today, potentially other install-time hints in the future), the table earns its place. If a future standard subsumes the remaining cases, the table can be removed without disturbing environment identity.

Alternatives Considered

Treat [tool.finecode.env.*] as the primary registry and regenerate [dependency-groups] from it as a codegen step. Rejected because it introduces a drift risk whenever a contributor edits pyproject.toml directly, and because it splits the source of truth in a way that makes debugging installation problems harder rather than easier. The current rule achieves the same single-source-of-truth property without a build step.

Introduce a FineCode-native environment composition primitive (for example [tool.finecode.env.dev].include = ["runtime"]) so that include-group never has to point at a FineCode-only environment. Rejected because FineCode has no current consumer for the primitive, it duplicates PEP 735 for marginal benefit, and it moves composition out of the standard surface that uv and pip already understand. Reopen this if FineCode ever needs composition semantics that PEP 735 cannot express.

Allow environments to be declared only under [tool.finecode.env.*] and have env discovery read from that table instead. Rejected because it cuts the config off from PEP 735 tooling for no upside: any such environment must still be installable, and installation is far simpler when the dependency list is already in the standard shape.

Add an empty runtime = [] stub to unblock a single include-group reference. Rejected because it leaves a configuration artifact that a non-FineCode reader cannot interpret without documentation, and because it does not address the underlying question of what [tool.finecode.env.*] is allowed to express on its own.