fix(analysis): skip inherited expose_dict / collect_dict via __dict__ lookup (#292)#298
Merged
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resolves #292.
Summary
_prepare_class_contextread__pydantic_resolve_expose__viagetattr(kls, ..., {}), which walks the MRO. A subclass that inherits the attribute without overriding it gets the parent's dict, and_validate_exposethen re-registers the same alias intoself.expose_set— falsely raisingValueError: Expose alias name conflictsthe moment both classes appear in the same resolve tree.Trigger
The conflict doesn't fire when
Childis the root (only one walk) — only when parent and child are both walked in the same tree.Fix
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 andcollect_set.addis idempotent), but the two lines are semantically identical and keeping them consistent prevents the same trap if collect validation logic changes later.Test plan
test_inherited_expose_does_not_conflict:ExposeParentwith expose config +ExposeChildinheriting + parent haslist[Child]field so both get walked. Pre-fix this raised; post-fix scan succeeds and child'sexpose_dictis{}.🤖 Generated with Claude Code