Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 24 additions & 4 deletions src/validkit/validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,35 @@


def _get_class_annotations(schema: type) -> Dict[str, Any]:
"""Return annotations declared on *schema*, including lazily exposed ones."""
"""Return annotations declared directly on *schema* (not inherited from base classes).

Prefers ``schema.__dict__["__annotations__"]`` to avoid Python 3.9's MRO traversal
behavior where ``getattr(cls, "__annotations__")`` returns a *parent* class's dict
when the child declares no annotations of its own.

The ``getattr`` fallback is kept only for metaclass-defined ``__annotations__``
properties (lazily computed annotations). In that case the returned mapping is a
freshly constructed object that cannot be ``is``-identical to any base class's
annotations dict, so the identity guard below correctly passes it through.
"""
own_annotations = schema.__dict__.get("__annotations__")
if isinstance(own_annotations, Mapping):
return dict(own_annotations)

# Fallback for metaclass-defined __annotations__ properties.
dynamic_annotations = getattr(schema, "__annotations__", None)
if isinstance(dynamic_annotations, Mapping):
return dict(dynamic_annotations)
return {}
if not isinstance(dynamic_annotations, Mapping):
return {}

# In Python 3.9, getattr traverses the class MRO and may return a parent
# class's __annotations__ dict as-is (same object identity). If the result
# is identical to any base class's own annotations, treat it as inherited and
# return an empty dict to avoid incorrectly pulling in parent-class fields.
for base in schema.__mro__[1:]:
if base.__dict__.get("__annotations__") is dynamic_annotations:
return {}

return dict(dynamic_annotations)


def _is_class_schema(schema: Any) -> bool:
Expand Down
Loading