Skip to content

feat: add block-scoped modifiers#72

Open
invokermain wants to merge 4 commits into
mainfrom
feat/block-scoped-modifiers
Open

feat: add block-scoped modifiers#72
invokermain wants to merge 4 commits into
mainfrom
feat/block-scoped-modifiers

Conversation

@invokermain
Copy link
Copy Markdown
Owner

Summary

  • Block-scoped modifiers: Modify declared inside a Block now only affects resolutions within that block's scope, matching fx/dig's scoped decorator behavior. Modifiers in BlockA don't leak to BlockB or top-level invocations.
  • Modifier composition: Block modifiers compose with global modifiers — a block modifier receives the globally-modified value as input (e.g., global prefix + block uppercase = "GLOBAL_FOO").
  • Bug fix: _bind_arguments (used by assemble()) now applies modifiers. Previously, invocations assembled via assemble() received raw provider values, bypassing all modifiers.

Implementation details

  • _ScopeNode extended with modifiers dict and find_modifier() chain walk (mirrors dig's storesToRoot())
  • Global modifiers moved from Assembler._modifiers onto root_node.modifiers
  • Block scope nodes are persistent _ScopeNode instances parented to root, created during Block.apply()
  • Engin.run() sets _scope_var to the block's node when assembling/invoking block invocations
  • Instance-level on-stack tracking (set[int] of modifier IDs) replaces type-level tracking to allow modifier composition across scope levels
  • Modified values cached on the modifier's source node (not always root) to prevent cross-scope leaking
  • find_modified() stops at nodes owning a modifier for the type, preventing inheritance of parent's cached modified value

Test plan

  • Block modifier only affects block's own invocations
  • Block modifier does not affect other blocks
  • Block + global modifier composition (block receives globally-modified value)
  • Global modifier works through Engin invocation path (not just Assembler.build)
  • All 143 existing + new tests pass
  • poe check passes (ruff format, ruff lint, mypy)

🤖 Generated with Claude Code

Modifiers declared inside a Block now only affect resolutions within
that block's scope, matching fx/dig's scoped decorator behavior.

Key changes:
- Extend _ScopeNode with modifiers field and find_modifier() chain walk
- Move global modifiers from Assembler._modifiers onto root _ScopeNode
- Fix _bind_arguments to apply modifiers (pre-existing bug where
  invocations assembled via assemble() received raw values)
- Add persistent block scope nodes to Engin, wired as children of root
- Route block modifiers to block scope nodes via Modify.apply()
- Set block scope context (via _scope_var) for assembly and invocation
- Support modifier composition: block modifier receives globally-modified
  value via instance-level on-stack tracking (not type-level)
- Cache modified values on modifier's source node to prevent leaking
  across scopes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 9, 2026

Codecov Report

❌ Patch coverage is 87.12121% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.72%. Comparing base (29f675e) to head (e769660).

Files with missing lines Patch % Lines
src/engin/_engin.py 88.37% 4 Missing and 1 partial ⚠️
src/engin/exceptions.py 44.44% 5 Missing ⚠️
src/engin/_assembler.py 93.54% 3 Missing and 1 partial ⚠️
src/engin/_dependency.py 70.00% 2 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #72      +/-   ##
==========================================
- Coverage   91.29%   90.72%   -0.57%     
==========================================
  Files          19       19              
  Lines        1309     1402      +93     
  Branches      184      201      +17     
==========================================
+ Hits         1195     1272      +77     
- Misses         69       83      +14     
- Partials       45       47       +2     
Flag Coverage Δ
macos-latest 89.94% <87.12%> (-0.51%) ⬇️
py3.10 90.37% <87.12%> (-0.54%) ⬇️
py3.11 90.37% <87.12%> (-0.54%) ⬇️
py3.12 90.37% <87.12%> (-0.54%) ⬇️
py3.13 90.37% <87.12%> (-0.54%) ⬇️
py3.14 90.37% <87.12%> (-0.55%) ⬇️
ubuntu-latest 89.94% <87.12%> (-0.51%) ⬇️
windows-latest 90.22% <87.12%> (-0.53%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

invokermain and others added 3 commits April 9, 2026 13:28
- modifiers.md: document block scoping behavior, composition with global
  modifiers, and update "one modifier per type" to "per type per scope"
- blocks.md: add tip about block-scoped modifier behavior
- CHANGELOG.md: remove outdated "global only" caveat from v0.4.0 entry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Block scope nodes now mirror the block nesting hierarchy. When
OuterBlock includes InnerBlock as an option, InnerBlock's scope node
is parented to OuterBlock's node (not root). This means inner block
invocations see outer block modifiers via the scope chain walk, matching
dig's nested scope behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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