Skip to content

Anchor a relative TaskEnv dependencies_from to the author's file (v0.1.18) - #61

Merged
Volv-G merged 1 commit into
masterfrom
piforge/tangle-pipeline-crud/fix-taskenv-relative-dependencie-0e9d4d8
Sep 23, 2026
Merged

Volv-G merged 1 commit into
masterfrom
piforge/tangle-pipeline-crud/fix-taskenv-relative-dependencie-0e9d4d8

Conversation

@Volv-G

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

Copy link
Copy Markdown
Collaborator

(AI-assisted)

A relative TaskEnv.dependencies_from resolved against the wrong file whenever it was passed to a TaskEnv subclass that overrides __post_init__.

__post_init__ reached the definition site with a fixed f_back.f_back, which assumes exactly one intermediate frame — the dataclass-generated __init__:

__post_init__  ->  generated __init__  ->  the author's file      # assumed

Every subclass that overrides __post_init__ and calls super() inserts another frame:

TaskEnv.__post_init__ -> SubEnv.__post_init__ -> generated __init__ -> the author's file
                                                 ^ f_back.f_back stopped here

A dataclass-generated __init__ is exec'd with the globals of the module that defines the dataclass, so f_globals["__file__"] was the subclass's own module — for a downstream env shipped in an installed package, a path inside that package — and the pip-dependency manifest silently pointed there. Downstream, DbtEnv(..., dependencies_from="rel.toml") written in /tmp/fr/probe.py resolved to .../site-packages/tangle_deploy/python_pipeline/dbt/rel.toml. from_config is unaffected (see below), so subclasses were the whole blast radius — and since from_config is documented as inherited by "any TaskEnv dataclass subclass", subclassing is a supported use.

Fix

Find the boundary by identity, not by counting frames: walk the stack and take the frame after the outermost frame that is still constructing this object.

A construction frame binds the object under construction to self — TaskEnv.__post_init__, every subclass __post_init__ that called super(), and the generated __init__ — and nothing outside the construction can hold that object yet, so the test is exact. The generated __init__ is additionally recognizable by its exec'd code object (co_filename == "<string>"). Adding frames no longer changes the answer, so the result is the author's file at any subclass depth.

"Outermost" rather than "first frame that is not self" matters: a subclass may reach the base validation through a plain helper function, whose frame binds the env under some other name. The generated __init__ further out still marks the boundary.

Unchanged:

  • Base TaskEnv(...) — same answer as before.
  • TaskEnv.from_config — keeps its own single-frame f_back lookup, which is correct and documented as not supporting a wrapping/delegating subclass, and keeps pre-resolving a relative dependencies_from against the config file's directory. (That pre-resolution is also why from_config never exercised the buggy walk: it hands the constructor an absolute path.)
  • Absolute paths — never re-anchored.
  • The working-directory fallback, still reached only when the construction site has no __file__ at all (exec'd or interactive code).

Tests

tests/test_task_env_dependencies_anchor.py — 8 tests. Each keeps three directories apart and asserts which one wins:

<tmp>/envlib/     the module DEFINING the subclasses   (the bug's answer)
<tmp>/project/    the module CONSTRUCTING the envs     (the correct answer)
<tmp>/elsewhere/  the working directory                (also wrong)

All three hold a deps.toml, so a misresolution lands on a file that exists and cannot hide behind a "file not found". The envs are always constructed from a written-to-disk author module rather than from the test file, so the expected anchor is a genuine third file and the frame stack is the real one.

Covered: depth 0, depth 1, depth 2, a subclass delegating through a library-level helper function, construction inside a function / a nested function / a method of an unrelated object, a subclass declared in the author's own file, absolute paths, and from_config anchoring for a depth-2 subclass.

Mutation-checked — the tests fail for the shipped code and for the obvious "one more frame" patch:

implementation failing tests
shipped f_back.f_back depth 1, depth 2, helper-delegating, function/method
f_back.f_back.f_back depth 0, depth 2, helper-delegating, function/method

The method case also rules out a naive "skip any frame with a self local": Factory().build() has a self that is a different object.

Full suite: 1548 passed on Python 3.13 and on 3.12 (the CI matrix). git diff --check clean.

Version

0.1.17 → 0.1.18 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). uv build produces tangle_cli-0.1.18, and both the wheel and the sdist report 0.1.18 from tangle version / tangle-cli version.

…1.18)

TaskEnv.__post_init__ reached the definition site with a fixed
f_back.f_back, which assumes exactly one intermediate frame: the
dataclass-generated __init__. Every subclass that overrides
__post_init__ and calls super() inserts another frame, so a relative
dependencies_from passed to such a subclass resolved against the module
that DEFINES the subclass -- for a downstream env that is the installed
package -- instead of the pipeline file that wrote the path.

Find the boundary by identity instead of by counting: walk the stack and
take the frame after the outermost frame still constructing this object.
A construction frame binds the object to self (the base and every
subclass __post_init__, plus the generated __init__, whose exec'd code
object is additionally identifiable by co_filename "<string>"); nothing
outside the construction can hold the object yet. Adding frames no
longer changes the answer, so the result is the author's file at any
dataclass subclass depth.

TaskEnv.from_config keeps its own single-frame lookup, which is correct
and documented as not supporting a wrapping subclass, and keeps
pre-resolving a relative dependencies_from against the config file's
directory.
@Volv-G
Volv-G requested a review from Ark-kun as a code owner September 23, 2026 12:26
@Volv-G
Volv-G merged commit d01a04b into master Sep 23, 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