Skip to content

fix(analysis): skip inherited expose_dict / collect_dict via __dict__ lookup (#292)#298

Merged
allmonday merged 1 commit into
masterfrom
fix/292-expose-inheritance-false-positive
Jun 24, 2026
Merged

fix(analysis): skip inherited expose_dict / collect_dict via __dict__ lookup (#292)#298
allmonday merged 1 commit into
masterfrom
fix/292-expose-inheritance-false-positive

Conversation

@allmonday

Copy link
Copy Markdown
Collaborator

Resolves #292.

Summary

_prepare_class_context read __pydantic_resolve_expose__ via getattr(kls, ..., {}), which walks the MRO. A subclass that inherits the attribute without overriding it gets the parent's dict, and _validate_expose then re-registers the same alias into self.expose_set — falsely raising ValueError: Expose alias name conflicts the moment both classes appear in the same resolve tree.

Trigger

class ExposeParent(BaseModel):
    __pydantic_resolve_expose__ = {'id': 'parent_id'}
    id: int
    siblings: list['ExposeChild'] = []   # forces walk into ExposeChild

class ExposeChild(BaseModel):
    name: str

await Resolver().resolve(ExposeParent(id=1, siblings=[]))
# pre-fix:  ValueError: Expose alias name conflicts
# post-fix: OK

The conflict doesn't fire when Child is the root (only one walk) — only when parent and child are both walked in the same tree.

Fix

- expose_dict = getattr(kls, const.EXPOSE_TO_DESCENDANT, {})
- collect_dict = getattr(kls, const.COLLECTOR_CONFIGURATION, {})
+ expose_dict = kls.__dict__.get(const.EXPOSE_TO_DESCENDANT, {})
+ collect_dict = kls.__dict__.get(const.COLLECTOR_CONFIGURATION, {})

kls.__dict__ only sees attributes defined directly on the class, so subclasses read {} unless they explicitly re-declare.

Symmetric change applied to collect_dict — that path doesn't currently false-positive (its check is by-reference and collect_set.add is idempotent), but the two lines are semantically identical and keeping them consistent prevents the same trap if collect validation logic changes later.

Test plan

  • Added test_inherited_expose_does_not_conflict: ExposeParent with expose config + ExposeChild inheriting + parent has list[Child] field so both get walked. Pre-fix this raised; post-fix scan succeeds and child's expose_dict is {}.
  • Original repro confirms: pre-fix raises, post-fix succeeds.
  • `pytest tests/` — 781 passed, 1 skipped.
  • `ruff check` — clean.

🤖 Generated with Claude Code

… lookup

_prepare_class_context used `getattr(kls, EXPOSE_TO_DESCENDANT, {})`,
which walks the MRO. When a subclass inherits __pydantic_resolve_expose__
without overriding it, getattr returns the parent's dict, and
_validate_expose re-registers the same alias into self.expose_set —
falsely raising "Expose alias name conflicts" the moment both classes
appear in the same resolve tree.

Read from kls.__dict__ instead, which only sees attributes defined
directly on the class. Inherited config now correctly reads as {} for
the subclass unless it explicitly re-declares.

Applied symmetrically to collect_dict — that path doesn't currently
false-positive (its check is by-reference, and collect_set.add is
idempotent), but the two lines are semantically identical ("read the
class's own declaration") and keeping them consistent prevents the same
trap if collect validation logic changes later.

Added test_inherited_expose_does_not_conflict in
test_analysis_edge_cases.py: ExposeParent with __pydantic_resolve_expose__
+ ExposeChild inheriting it + parent has list[Child] field, so both
classes get walked. Pre-fix this raised; post-fix the scan succeeds and
the child's expose_dict is {}.

Repro before fix:
  ValueError: Expose alias name conflicts, please check: __main__.Sibling
After fix:
  resolve 成功

Tests: 781 passed, 1 skipped. Ruff clean.

Resolves #292.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@allmonday allmonday merged commit ab4e626 into master Jun 24, 2026
1 check 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.

bug: analysis._validate_expose 对继承自父类的 expose_dict 误报 alias 冲突

1 participant