Skip to content

fix(resolver): port v5.10.3 state isolation + collector + context fixes#88

Merged
allmonday merged 1 commit into
masterfrom
fix/resolver-v5.10.3-ports
Jun 25, 2026
Merged

fix(resolver): port v5.10.3 state isolation + collector + context fixes#88
allmonday merged 1 commit into
masterfrom
fix/resolver-v5.10.3-ports

Conversation

@allmonday

Copy link
Copy Markdown
Collaborator

Summary

Three bug-fix ports from pydantic-resolve v5.10.3 that apply to nexusx's Resolver / Collector surface:

  • #289-2 — concurrent resolve guard. Resolver holds per-call mutable state on self (_node_collectors, _loader_cache, the levels list inside _traverse). Two overlapping resolve() calls on the same instance clobbered each other and surfaced as a cryptic KeyError. Added a _in_resolve flag + try/finally that raises a clear RuntimeError instead.
  • #293 — Collector subclass preservation. _phase_b_prepare_collectors hardcoded Collector(alias=alias, flat=flat) when instantiating per-node collectors, silently downgrading any ICollector implementation or Collector subclass with extra __init__ config (key_fn, n, dict-valued aggregator) to the base Collector. Now deepcopys the prototype, and the isinstance(param.default, Collector) check widened to ICollector so direct implementations are detected.
  • #291 — context input validation. Resolver(context={}) was silently coerced to {} via context or {}, and non-dict values slipped through to confuse _traverse. Now rejects empty dict with ValueError and non-dict with TypeError at __init__.

The other two v5.10.3 fixes were evaluated and intentionally skipped — see Test plan notes.

Test plan

  • tests/test_resolver_concurrency.py — 4 new tests for the concurrent-call guard
  • tests/test_context.py::TestCollectorSubclass — 5 new tests migrated from pydantic-resolve test_collector_subclass.py
  • tests/test_resolver_context_validation.py — 8 new tests for context validation (5 parametrized)
  • Full suite: 1025 passed, 6 skipped (6 skipped are pre-existing Python 3.14-only PEP 649 tests)
  • ruff check clean

N/A fixes (documented for reviewers):

  • #290 (Profile.Timer.timeset leak) — nexusx has no Timer / profile module; only uses logging.
  • #292 (inherited __pydantic_resolve_expose__ via getattr MRO false-positive) — nexusx's scan_expose_fields reads model_fields[field].metadata directly, not getattr(kls, '__xxx__'), so the MRO false-positive doesn't exist.
  • #289-1 (object_level_collect_alias_map_store accumulating across calls) — already handled: _node_collectors.clear() and _loader_cache.clear() run at the top of every resolve().
  • #289-3 (dict.get(k, expensive_default) evaluating default eagerly) — no such pattern in nexusx's resolver (audited every .get( call).

🤖 Generated with Claude Code

Three bug fixes from pydantic-resolve v5.10.3 that apply to nexusx:

#289-2 (concurrent resolve guard): Resolver holds per-call mutable state
on self (_node_collectors, _loader_cache, levels list). Two overlapping
resolve() calls on the same instance clobbered each other and surfaced
as a cryptic KeyError. Added _in_resolve flag + try/finally that raises
a clear RuntimeError instead.

#293 (Collector subclass preservation): _phase_b_prepare_collectors
hardcoded `Collector(alias=alias, flat=flat)` when instantiating
per-node collectors, silently downgrading any ICollector implementation
or Collector subclass with extra __init__ config (key_fn, n, dict-valued
aggregator) to the base Collector. Now deepcopies the prototype, and
the isinstance check widened from Collector to ICollector so direct
ICollector implementations are detected.

#291 (context input validation): Resolver(context={}) was silently
coerced to {} via `context or {}`, and non-dict values (Resolver(context=
[1,2,3])) slipped through to confuse _traverse. Now rejects empty dict
with ValueError and non-dict with TypeError at __init__.

Also evaluates the other two v5.10.3 fixes — #290 (Timer.timeset leak)
and #292 (inherited __pydantic_resolve_expose__ via getattr MRO) — and
documents why neither applies to nexusx (no Timer module; expose scan
reads model_fields.metadata, not class-level __xxx__ attrs).

Tests: +17 migrated from pydantic-resolve
- test_resolver_concurrency.py (4): concurrent reject, sequential reuse,
  guard resets after exception, separate instances concurrent ok
- test_context.py::TestCollectorSubclass (5): MapCollector dedupe,
  sibling branch isolation, sequential no-leak, TopNCollector preserves
  n, SimpleSubCollector backward-compat baseline
- test_resolver_context_validation.py (8): None / non-empty dict allowed,
  empty dict ValueError, 5 parametrized non-dict TypeError

Full suite: 1025 passed, 6 skipped (3.14-only).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@allmonday allmonday merged commit 17a0626 into master Jun 25, 2026
5 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