diff --git a/pydantic_resolve/analysis.py b/pydantic_resolve/analysis.py index ac06d0c4..c9b181f5 100644 --- a/pydantic_resolve/analysis.py +++ b/pydantic_resolve/analysis.py @@ -588,8 +588,11 @@ def _prepare_class_context(self, kls: type, kls_name: str) -> FieldContext: object_fields_without_resolver = [a[0] for a in object_fields if a[0] not in fields_with_resolver] # Scan expose and collect (__pydantic_resolve_xxx__) - expose_dict = getattr(kls, const.EXPOSE_TO_DESCENDANT, {}) - collect_dict = getattr(kls, const.COLLECTOR_CONFIGURATION, {}) + # Read from kls.__dict__ to skip inherited attributes — a subclass that + # doesn't override __pydantic_resolve_expose__ would otherwise re-register + # the parent's aliases and falsely trigger the conflict check (#292). + expose_dict = kls.__dict__.get(const.EXPOSE_TO_DESCENDANT, {}) + collect_dict = kls.__dict__.get(const.COLLECTOR_CONFIGURATION, {}) self._validate_expose(expose_dict, kls_name) return { diff --git a/tests/analysis/test_analysis_edge_cases.py b/tests/analysis/test_analysis_edge_cases.py index 2d37513f..1b2e9dd4 100644 --- a/tests/analysis/test_analysis_edge_cases.py +++ b/tests/analysis/test_analysis_edge_cases.py @@ -100,6 +100,37 @@ def test_expose_conflict(): Analytic().scan(ExposeConflictModel) +# ============ Test 4b: Inherited expose_dict must not double-register ============ +# See #292: a subclass that inherits __pydantic_resolve_expose__ without +# overriding it used to re-register the parent's aliases via getattr-MRO +# lookup, falsely raising "Expose alias name conflicts" when both classes +# appear in the same resolve tree. +class ExposeParent(BaseModel): + __pydantic_resolve_expose__ = {'id': 'parent_id'} + id: int + siblings: list['ExposeChild'] = [] + + +class ExposeChild(BaseModel): + name: str + + +ExposeParent.model_rebuild() + + +def test_inherited_expose_does_not_conflict(): + """A subclass that doesn't override __pydantic_resolve_expose__ should + not re-register the parent's aliases when walked in the same tree.""" + # Should not raise — ExposeChild inherits the field but contributes no + # expose declaration of its own. + result = Analytic().scan(ExposeParent) + prefix = 'tests.analysis.test_analysis_edge_cases' + parent_expose = result[f'{prefix}.ExposeParent']['expose_dict'] + assert parent_expose == {'id': 'parent_id'} + child_expose = result[f'{prefix}.ExposeChild']['expose_dict'] + assert child_expose == {} + + # ============ Test 5: Inheritance chain ============ class BaseModelWithResolve(BaseModel): id: int