Skip to content

Load a TaskEnv from a config file with TaskEnv.from_config (v0.1.17) - #60

Merged
Volv-G merged 1 commit into
masterfrom
piforge/tangle-pipeline-crud/taskenv-from-config-generic-af5c079
Sep 19, 2026
Merged

Volv-G merged 1 commit into
masterfrom
piforge/tangle-pipeline-crud/taskenv-from-config-generic-af5c079

Conversation

@Volv-G

@Volv-G Volv-G commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

(AI-assisted)

Adds a generic TaskEnv.from_config(path) so an execution environment can be declared in a YAML/JSON file instead of hardcoded in the pipeline module. Every TaskEnv dataclass subclass inherits it, returns its own type, and is validated against its own fields.

ENV = TaskEnv.from_config("tangle/envs.yaml")  # relative to THIS file

TaskEnv.from_config is the only new public name in this PR.

Behaviour

  • A relative path resolves against the calling file's directory, never the working directory, so a pipeline script selects the same config wherever the CLI is run from. No __file__ (exec/REPL) is a clear error, never a silent CWD fallback.
  • The document is read with ArgsContainer._load_config_file — the loader --config already uses — so the _select directive and the accepted document shapes cannot drift from the CLI. It must resolve to exactly one object.
  • Accepted keys come from inspect.signature(cls) rather than dataclasses.fields(), because that is what cls(**case) actually accepts: InitVar pseudo-fields in, ClassVar and field(init=False) out. *args/**kwargs are excluded so a typo stays fail-closed. Unknown keys are rejected with the allowed names, which keeps these files environment-only — pipeline concerns like versioning or schedules have no field to land in.
  • A relative dependencies_from in the object anchors to the config file's directory rather than the module's.

Diagnostics never echo a config value

A config file is untrusted input, so nothing from it reaches a compile/CI log.

Keys go through the existing capped, control-character-scrubbing renderer, widened to accept non-string YAML keys and now applied to the _select sibling/unexpected paths that previously used bare repr().

A constructor's own TypeError/ValueError text is never quoted — it may embed a value directly, as a leaf nested inside a structure, or transformed (lower cased, sliced, re-encoded), and no content scan can undo a transformation. The rejected exception is also kept off __cause__ and __context__, because traceback.format_exception prints a chain verbatim; the error is raised after leaving the except handler, since raise ... from None would still leave the object reachable on __context__.

GpuEnv.from_config: /repo/envs.yaml case is not a valid GpuEnv (fields present:
accelerator, image). Its validation message is withheld because it can contain
config values; construct GpuEnv(...) directly to see it.

Calling GpuEnv(...) directly is unaffected and raises the ordinary ValueError in full.

Tests

tests/test_task_env_from_config.py — 29 tests: caller-relative and absolute resolution, config-file-relative dependencies_from, _select with and without a default, subclass field/type inheritance, InitVar/ClassVar/init=False acceptance, environment-only rejection, and an end-to-end compile of a generated task. Disclosure is covered for raw, nested-leaf, transformed, and single-character values, asserted against the rendered traceback rather than just str(exc). Reintroducing the exception chain fails 5 of them.

Full suite: 1540 passed. git diff --check clean.

Version

0.1.160.1.17 across the four surfaces: pyproject.toml, packages/tangle-cli/src/tangle_cli/__init__.py, tests/test_packaging.py, and the editable self-entry in uv.lock (hand-edited, 1 1 diff; the lockfile was not regenerated).

Adds a generic `TaskEnv.from_config(path)` so an execution environment can
be declared in a YAML/JSON file instead of hardcoded in the pipeline module.
Every `TaskEnv` dataclass subclass inherits it, returns its own type, and is
validated against its own fields.

- A relative path resolves against the calling file, never the working
  directory, so a pipeline selects the same config wherever the CLI runs.
- The document is read with `ArgsContainer._load_config_file`, the loader
  `--config` already uses, so `_select` and the accepted document shapes
  cannot drift from the CLI. It must resolve to exactly one object.
- Accepted keys come from `inspect.signature(cls)` rather than
  `dataclasses.fields()`, which is what `cls(**case)` actually accepts:
  InitVar pseudo-fields in, ClassVar and `field(init=False)` out. Unknown
  keys are rejected with the allowed names, keeping these files
  environment-only.
- A relative `dependencies_from` anchors to the config file's directory.

A config file is untrusted input, so no diagnostic echoes a config value.
Keys go through the existing capped, control-character-scrubbing renderer,
now widened to accept non-string YAML keys and applied to the `_select`
sibling/unexpected paths that previously used bare `repr()`. A constructor's
own exception text is never quoted, since it may embed a value directly,
nested inside a structure, or transformed; the rejected exception is also
kept off `__cause__`/`__context__`, which a rendered traceback would
otherwise print into the same CI log. The message names the class and the
fields present and points the author at constructing the class directly,
which is unchanged and still raises the ordinary `ValueError`.

`TaskEnv.from_config` is the only new public name.

Assisted-By: devx/bc7bacf9-958c-4255-ab2f-d2d5b96647b8
@Volv-G
Volv-G requested a review from Ark-kun as a code owner September 19, 2026 01:41
@Volv-G
Volv-G merged commit 62fb549 into master Sep 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant